mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
Refactor web UI when running actions and sort out dependencies
This commit is contained in:
parent
d4751a9d3f
commit
a681284bf7
39 changed files with 716 additions and 688 deletions
|
|
@ -3,7 +3,7 @@ from traceback import format_exc
|
|||
|
||||
def pre_render(**kwargs):
|
||||
try:
|
||||
data = kwargs["app"].bw_instances_utils.get_metrics("antibot")
|
||||
data = kwargs["bw_instances_utils"].get_metrics("antibot")
|
||||
return {
|
||||
"counter_failed_challenges": {
|
||||
"value": data.get("counter_failed_challenges", 0),
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ from logging import debug
|
|||
from traceback import format_exc
|
||||
|
||||
|
||||
def pre_render(app, *args, **kwargs):
|
||||
def pre_render(*args, **kwargs):
|
||||
try:
|
||||
backup_file = app.db.get_job_cache_file("backup-data", "backup.json")
|
||||
backup_file = kwargs["db"].get_job_cache_file("backup-data", "backup.json")
|
||||
debug(f"backup_file: {backup_file}")
|
||||
|
||||
data = loads(backup_file or "{}")
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from traceback import format_exc
|
|||
def pre_render(**kwargs):
|
||||
try:
|
||||
# Here we will have a list { 'counter_403': X, 'counter_401': Y ... }
|
||||
data = kwargs["app"].bw_instances_utils.get_metrics("badbehavior")
|
||||
data = kwargs["bw_instances_utils"].get_metrics("badbehavior")
|
||||
# Format to fit [{code: 403, count: X}, {code: 401, count: Y} ...]
|
||||
format_data = [{"code": int(key.split("_")[1]), "count": int(value)} for key, value in data.items()]
|
||||
format_data.sort(key=itemgetter("count"), reverse=True)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ def pre_render(**kwargs):
|
|||
}
|
||||
|
||||
try:
|
||||
data = kwargs["app"].bw_instances_utils.get_metrics("blacklist")
|
||||
data = kwargs["bw_instances_utils"].get_metrics("blacklist")
|
||||
for key in metrics:
|
||||
metrics[key]["value"] = data.get(key, 0)
|
||||
return metrics
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from traceback import format_exc
|
|||
|
||||
def pre_render(**kwargs):
|
||||
try:
|
||||
ping_data = kwargs["app"].bw_instances_utils.get_ping("bunkernet")
|
||||
ping_data = kwargs["bw_instances_utils"].get_ping("bunkernet")
|
||||
return {"ping_status": {"title": "BUNKERNET STATUS", "value": ping_data["status"]}}
|
||||
except BaseException:
|
||||
print(format_exc(), flush=True)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from traceback import format_exc
|
|||
|
||||
def pre_render(**kwargs):
|
||||
try:
|
||||
data = kwargs["app"].bw_instances_utils.get_metrics("cors")
|
||||
data = kwargs["bw_instances_utils"].get_metrics("cors")
|
||||
return {
|
||||
"counter_failed_cors": {
|
||||
"value": data.get("counter_failed_cors", 0),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from traceback import format_exc
|
|||
|
||||
def pre_render(**kwargs):
|
||||
try:
|
||||
data = kwargs["app"].bw_instances_utils.get_metrics("country")
|
||||
data = kwargs["bw_instances_utils"].get_metrics("country")
|
||||
return {
|
||||
"counter_failed_country": {
|
||||
"value": data.get("counter_failed_country", 0),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from traceback import format_exc
|
|||
|
||||
def pre_render(**kwargs):
|
||||
try:
|
||||
data = kwargs["app"].bw_instances_utils.get_metrics("dnsbl")
|
||||
data = kwargs["bw_instances_utils"].get_metrics("dnsbl")
|
||||
return {
|
||||
"counter_failed_dnsbl": {
|
||||
"value": data.get("counter_failed_dnsbl", 0),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from traceback import format_exc
|
|||
def pre_render(**kwargs):
|
||||
try:
|
||||
# Here we will have a list { 'counter_403': X, 'counter_401': Y ... }
|
||||
data = kwargs["app"].bw_instances_utils.get_metrics("errors")
|
||||
data = kwargs["bw_instances_utils"].get_metrics("errors")
|
||||
# Format to fit [{code: 403, count: X}, {code: 401, count: Y} ...]
|
||||
format_data = [{"code": int(key.split("_")[1]), "count": int(value)} for key, value in data.items()]
|
||||
format_data.sort(key=itemgetter("count"), reverse=True)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from traceback import format_exc
|
|||
|
||||
def pre_render(**kwargs):
|
||||
try:
|
||||
data = kwargs["app"].bw_instances_utils.get_metrics("greylist")
|
||||
data = kwargs["bw_instances_utils"].get_metrics("greylist")
|
||||
return {
|
||||
"counter_failed_greylist": {
|
||||
"value": data.get("counter_failed_greylist", 0),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from traceback import format_exc
|
|||
def pre_render(**kwargs):
|
||||
try:
|
||||
# Here we will have a list { 'limit_uri_url1': X, 'limit_uri_url2': Y ... }
|
||||
data = kwargs["app"].bw_instances_utils.get_metrics("limit")
|
||||
data = kwargs["bw_instances_utils"].get_metrics("limit")
|
||||
format_data = []
|
||||
# Format to fit [{url: "url1", count: X}, {url: "url2", count: Y} ...]
|
||||
for key, value in data.items():
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from traceback import format_exc
|
|||
|
||||
def pre_render(**kwargs):
|
||||
try:
|
||||
data = kwargs["app"].bw_instances_utils.get_metrics("misc")
|
||||
data = kwargs["bw_instances_utils"].get_metrics("misc")
|
||||
|
||||
return {
|
||||
"counter_failed_default": {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ def pre_render(**kwargs):
|
|||
data = {}
|
||||
error = ""
|
||||
try:
|
||||
ping_data = kwargs["app"].bw_instances_utils.get_ping("redis")
|
||||
ping_data = kwargs["bw_instances_utils"].get_ping("redis")
|
||||
ping = {"ping_status": {"title": "REDIS STATUS", "value": ping_data["status"]}}
|
||||
except BaseException:
|
||||
print(format_exc(), flush=True)
|
||||
|
|
@ -14,7 +14,7 @@ def pre_render(**kwargs):
|
|||
ping = {"ping_status": {"title": "REDIS STATUS", "value": "error"}}
|
||||
|
||||
try:
|
||||
metrics = kwargs["app"].bw_instances_utils.get_metrics("redis")
|
||||
metrics = kwargs["bw_instances_utils"].get_metrics("redis")
|
||||
data = {
|
||||
"counter_redis_nb_keys": {
|
||||
"value": metrics.get("redis_nb_keys", 0),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from traceback import format_exc
|
|||
def pre_render(**kwargs):
|
||||
try:
|
||||
# Here we will have a list { 'counter_403': X, 'counter_401': Y ... }
|
||||
data = kwargs["app"].bw_instances_utils.get_metrics("reversescan")
|
||||
data = kwargs["bw_instances_utils"].get_metrics("reversescan")
|
||||
# Format to fit [{code: 403, count: X}, {code: 401, count: Y} ...]
|
||||
format_data = [{"port": int(key.split("_")[-1]), "count": value} for key, value in data.items()]
|
||||
format_data.sort(key=itemgetter("count"), reverse=True)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from traceback import format_exc
|
|||
|
||||
def pre_render(**kwargs):
|
||||
try:
|
||||
data = kwargs["app"].bw_instances_utils.get_metrics("whitelist")
|
||||
data = kwargs["bw_instances_utils"].get_metrics("whitelist")
|
||||
return {
|
||||
"counter_passed_whitelist": {
|
||||
"value": data.get("counter_passed_whitelist", 0),
|
||||
|
|
|
|||
|
|
@ -2078,7 +2078,9 @@ class Database:
|
|||
|
||||
return ""
|
||||
|
||||
def update_external_plugins(self, plugins: List[Dict[str, Any]], *, _type: Literal["external", "pro"] = "external", delete_missing: bool = True) -> str:
|
||||
def update_external_plugins(
|
||||
self, plugins: List[Dict[str, Any]], *, _type: Literal["external", "ui", "pro"] = "external", delete_missing: bool = True
|
||||
) -> str:
|
||||
"""Update external plugins from the database"""
|
||||
to_put = []
|
||||
changes = False
|
||||
|
|
@ -2869,6 +2871,38 @@ class Database:
|
|||
|
||||
return ""
|
||||
|
||||
def delete_plugin(self, plugin_id: str, method: str) -> str:
|
||||
"""Delete a plugin from the database."""
|
||||
with self._db_session() as session:
|
||||
plugin = session.query(Plugins).filter_by(id=plugin_id, method=method).first()
|
||||
if not plugin:
|
||||
return f"Plugin with id {plugin_id} and method {method} not found"
|
||||
|
||||
session.query(Plugins).filter_by(id=plugin_id, method=method).delete()
|
||||
session.query(Settings).filter_by(plugin_id=plugin_id).delete()
|
||||
session.query(Selects).filter(Selects.setting_id.in_(session.query(Settings).filter_by(plugin_id=plugin_id).with_entities(Settings.id))).delete()
|
||||
session.query(Jobs).filter_by(plugin_id=plugin_id).delete()
|
||||
session.query(Jobs_cache).filter_by(plugin_id=plugin_id).delete()
|
||||
session.query(Jobs_runs).filter_by(plugin_id=plugin_id).delete()
|
||||
session.query(Plugin_pages).filter_by(plugin_id=plugin_id).delete()
|
||||
session.query(Bw_cli_commands).filter_by(plugin_id=plugin_id).delete()
|
||||
session.query(Templates).filter_by(plugin_id=plugin_id).delete()
|
||||
session.query(Template_steps).filter(
|
||||
Template_steps.template_id.in_(session.query(Templates).filter_by(plugin_id=plugin_id).with_entities(Templates.id))
|
||||
).delete()
|
||||
session.query(Template_settings).filter(
|
||||
Template_settings.template_id.in_(session.query(Templates).filter_by(plugin_id=plugin_id).with_entities(Templates.id))
|
||||
).delete()
|
||||
session.query(Template_custom_configs).filter(
|
||||
Template_custom_configs.template_id.in_(session.query(Templates).filter_by(plugin_id=plugin_id).with_entities(Templates.id))
|
||||
).delete()
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
except BaseException as e:
|
||||
return str(e)
|
||||
return ""
|
||||
|
||||
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 = []
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ INTEGRATIONS_ENUM = Enum(
|
|||
name="integrations_enum",
|
||||
)
|
||||
STREAM_TYPES_ENUM = Enum("no", "yes", "partial", name="stream_types_enum")
|
||||
PLUGIN_TYPES_ENUM = Enum("core", "external", "pro", name="plugin_types_enum")
|
||||
PLUGIN_TYPES_ENUM = Enum("core", "external", "ui", "pro", name="plugin_types_enum")
|
||||
PRO_STATUS_ENUM = Enum("active", "invalid", "expired", "suspended", name="pro_status_enum")
|
||||
INSTANCE_TYPE_ENUM = Enum("static", "container", "pod", name="instance_type_enum")
|
||||
INSTANCE_STATUS_ENUM = Enum("loading", "up", "down", name="instance_status_enum")
|
||||
|
|
|
|||
15
src/ui/dependencies.py
Normal file
15
src/ui/dependencies.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from logging import getLogger
|
||||
from os import sep
|
||||
from pathlib import Path
|
||||
|
||||
from src.config import Config
|
||||
from src.instance import InstancesUtils
|
||||
from src.ui_data import UIData
|
||||
|
||||
from ui_database import UIDatabase
|
||||
|
||||
DB = UIDatabase(getLogger("UI"), log=False)
|
||||
DATA = UIData(Path(sep, "var", "tmp", "bunkerweb").joinpath("ui_data.json"))
|
||||
|
||||
BW_CONFIG = Config(DB)
|
||||
BW_INSTANCES_UTILS = InstancesUtils(DB)
|
||||
|
|
@ -7,7 +7,6 @@ from secrets import token_urlsafe
|
|||
from sys import exit, path as sys_path
|
||||
from time import sleep
|
||||
|
||||
|
||||
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:
|
||||
sys_path.append(deps_path)
|
||||
|
|
@ -56,6 +55,8 @@ def on_starting(server):
|
|||
RUN_DIR.mkdir(parents=True, exist_ok=True)
|
||||
LIB_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
TMP_DIR.joinpath("ui_data.json").write_text("{}", encoding="utf-8")
|
||||
|
||||
LOGGER = setup_logger("UI", getenv("CUSTOM_LOG_LEVEL", getenv("LOG_LEVEL", "INFO")))
|
||||
|
||||
FLASK_SECRET = getenv("FLASK_SECRET")
|
||||
|
|
@ -191,3 +192,4 @@ def on_exit(server):
|
|||
RUN_DIR.joinpath("ui.pid").unlink(missing_ok=True)
|
||||
TMP_DIR.joinpath("ui.healthy").unlink(missing_ok=True)
|
||||
TMP_DIR.joinpath(".flask_secret").unlink(missing_ok=True)
|
||||
TMP_DIR.joinpath("ui_data.json").unlink(missing_ok=True)
|
||||
|
|
|
|||
183
src/ui/main.py
183
src/ui/main.py
|
|
@ -1,10 +1,9 @@
|
|||
#!/usr/bin/env python3
|
||||
from contextlib import suppress
|
||||
from os import _exit, getenv, sep
|
||||
from os import getenv, sep
|
||||
from os.path import join
|
||||
from secrets import token_urlsafe
|
||||
from sys import path as sys_path
|
||||
from pathlib import Path
|
||||
|
||||
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:
|
||||
|
|
@ -15,19 +14,11 @@ from flask import Flask, Response, flash, jsonify, make_response, redirect, rend
|
|||
from flask_login import current_user, LoginManager, login_required, logout_user
|
||||
from flask_principal import ActionNeed, identity_loaded, Permission, Principal, RoleNeed, TypeNeed, UserNeed
|
||||
from flask_wtf.csrf import CSRFProtect, CSRFError
|
||||
from json import JSONDecodeError, dumps, loads as json_loads
|
||||
from json import dumps
|
||||
from signal import SIGINT, signal, SIGTERM
|
||||
from subprocess import PIPE, Popen, call
|
||||
from time import time
|
||||
|
||||
from logger import setup_logger # type: ignore
|
||||
|
||||
from src.instance import InstancesUtils
|
||||
from src.custom_config import CustomConfig
|
||||
from src.config import Config
|
||||
from src.reverse_proxied import ReverseProxied
|
||||
from src.totp import Totp
|
||||
from src.ui_data import UIData
|
||||
|
||||
from pages.bans import bans
|
||||
from pages.cache import cache
|
||||
|
|
@ -47,75 +38,27 @@ from pages.services import services
|
|||
from pages.setup import setup
|
||||
from pages.totp import totp
|
||||
|
||||
from dependencies import BW_CONFIG, DATA, DB
|
||||
from models import AnonymousUser
|
||||
from ui_database import UIDatabase
|
||||
from utils import check_settings
|
||||
|
||||
TMP_DIR = Path(sep, "var", "tmp", "bunkerweb")
|
||||
LIB_DIR = Path(sep, "var", "lib", "bunkerweb")
|
||||
|
||||
|
||||
def stop_gunicorn():
|
||||
p = Popen(["pgrep", "-f", "gunicorn"], stdout=PIPE)
|
||||
out, _ = p.communicate()
|
||||
pid = out.strip().decode().split("\n")[0]
|
||||
call(["kill", "-SIGTERM", pid])
|
||||
|
||||
|
||||
def stop(status, _stop=True):
|
||||
Path(sep, "var", "run", "bunkerweb", "ui.pid").unlink(missing_ok=True)
|
||||
TMP_DIR.joinpath("ui.healthy").unlink(missing_ok=True)
|
||||
if _stop is True:
|
||||
stop_gunicorn()
|
||||
_exit(status)
|
||||
|
||||
|
||||
def handle_stop(signum, frame):
|
||||
app.logger.info("Caught stop operation")
|
||||
app.logger.info("Stopping web ui ...")
|
||||
stop(0, False)
|
||||
|
||||
from utils import TMP_DIR, LOGGER, check_settings, handle_stop, stop
|
||||
|
||||
signal(SIGINT, handle_stop)
|
||||
signal(SIGTERM, handle_stop)
|
||||
|
||||
sbin_nginx_path = Path(sep, "usr", "sbin", "nginx")
|
||||
|
||||
# Flask app
|
||||
app = Flask(__name__, static_url_path="/", static_folder="static", template_folder="templates")
|
||||
|
||||
with app.app_context():
|
||||
PROXY_NUMBERS = int(getenv("PROXY_NUMBERS", "1"))
|
||||
app.wsgi_app = ReverseProxied(app.wsgi_app, x_for=PROXY_NUMBERS, x_proto=PROXY_NUMBERS, x_host=PROXY_NUMBERS, x_prefix=PROXY_NUMBERS)
|
||||
app.logger = setup_logger("UI", getenv("CUSTOM_LOG_LEVEL", getenv("LOG_LEVEL", "INFO")))
|
||||
|
||||
FLASK_SECRET = getenv("FLASK_SECRET")
|
||||
if not FLASK_SECRET:
|
||||
if not TMP_DIR.joinpath(".flask_secret").is_file():
|
||||
app.logger.error("The FLASK_SECRET environment variable is missing and the .flask_secret file is missing, exiting ...")
|
||||
LOGGER.error("The FLASK_SECRET environment variable is missing and the .flask_secret file is missing, exiting ...")
|
||||
stop(1)
|
||||
FLASK_SECRET = TMP_DIR.joinpath(".flask_secret").read_text(encoding="utf-8").strip()
|
||||
|
||||
TOTP_SECRETS = getenv("TOTP_SECRETS", "")
|
||||
if TOTP_SECRETS:
|
||||
try:
|
||||
TOTP_SECRETS = json_loads(TOTP_SECRETS)
|
||||
except JSONDecodeError:
|
||||
x = 1
|
||||
tmp_secrets = {}
|
||||
for secret in TOTP_SECRETS.strip().split(" "):
|
||||
if secret:
|
||||
tmp_secrets[x] = secret
|
||||
x += 1
|
||||
TOTP_SECRETS = tmp_secrets.copy()
|
||||
del tmp_secrets
|
||||
|
||||
if not TOTP_SECRETS:
|
||||
if not LIB_DIR.joinpath(".totp_secrets.json").is_file():
|
||||
app.logger.error("The TOTP_SECRETS environment variable is missing and the .totp_secrets.json file is missing, exiting ...")
|
||||
stop(1)
|
||||
TOTP_SECRETS = json_loads(LIB_DIR.joinpath(".totp_secrets.json").read_text(encoding="utf-8"))
|
||||
|
||||
app.config["SECRET_KEY"] = FLASK_SECRET
|
||||
|
||||
app.config["SESSION_COOKIE_NAME"] = "__Host-bw_ui_session"
|
||||
|
|
@ -142,8 +85,6 @@ with app.app_context():
|
|||
login_manager.login_view = "login.login_page"
|
||||
login_manager.anonymous_user = AnonymousUser
|
||||
|
||||
app.db = UIDatabase(app.logger, log=False)
|
||||
|
||||
# Declare functions for jinja2
|
||||
app.jinja_env.globals.update(
|
||||
check_settings=check_settings,
|
||||
|
|
@ -156,38 +97,32 @@ with app.app_context():
|
|||
csrf = CSRFProtect()
|
||||
csrf.init_app(app)
|
||||
|
||||
app.bw_instances_utils = InstancesUtils(app.db)
|
||||
app.bw_config = Config(app.db)
|
||||
app.bw_custom_configs = CustomConfig()
|
||||
app.data = UIData(TMP_DIR.joinpath("ui_data.json"))
|
||||
app.totp = Totp(app, TOTP_SECRETS)
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def inject_variables():
|
||||
app.data.load_from_file()
|
||||
metadata = app.db.get_metadata()
|
||||
DATA.load_from_file()
|
||||
metadata = DB.get_metadata()
|
||||
|
||||
changes_ongoing = any(
|
||||
v
|
||||
for k, v in app.db.get_metadata().items()
|
||||
for k, v in DB.get_metadata().items()
|
||||
if k in ("custom_configs_changed", "external_plugins_changed", "pro_plugins_changed", "plugins_config_changed", "instances_changed")
|
||||
)
|
||||
|
||||
if not changes_ongoing and app.data.get("PRO_LOADING"):
|
||||
app.data["PRO_LOADING"] = False
|
||||
if not changes_ongoing and DATA.get("PRO_LOADING"):
|
||||
DATA["PRO_LOADING"] = False
|
||||
|
||||
if not changes_ongoing and metadata["failover"]:
|
||||
flash(
|
||||
"The last changes could not be applied because it creates a configuration error on NGINX, please check the logs for more information. The configured fell back to the last working one.",
|
||||
"error",
|
||||
)
|
||||
elif not changes_ongoing and not metadata["failover"] and app.data.get("CONFIG_CHANGED", False):
|
||||
elif not changes_ongoing and not metadata["failover"] and DATA.get("CONFIG_CHANGED", False):
|
||||
flash("The last changes have been applied successfully.", "success")
|
||||
app.data["CONFIG_CHANGED"] = False
|
||||
DATA["CONFIG_CHANGED"] = False
|
||||
|
||||
# Keep only plugins with a page to display on sidebar
|
||||
plugins_page = [{"id": plugin.get("id"), "name": plugin.get("name")} for plugin in app.bw_config.get_plugins() if plugin.get("page", False)]
|
||||
plugins_page = [{"id": plugin.get("id"), "name": plugin.get("name")} for plugin in BW_CONFIG.get_plugins() if plugin.get("page", False)]
|
||||
|
||||
# check that is value is in tuple
|
||||
return dict(
|
||||
|
|
@ -198,10 +133,10 @@ def inject_variables():
|
|||
pro_services=metadata["pro_services"],
|
||||
pro_expire=metadata["pro_expire"].strftime("%Y-%m-%d") if metadata["pro_expire"] else "Unknown",
|
||||
pro_overlapped=metadata["pro_overlapped"],
|
||||
plugins=app.bw_config.get_plugins(),
|
||||
pro_loading=app.data.get("PRO_LOADING", False),
|
||||
plugins=BW_CONFIG.get_plugins(),
|
||||
pro_loading=DATA.get("PRO_LOADING", False),
|
||||
bw_version=metadata["version"],
|
||||
is_readonly=app.data.get("READONLY_MODE", False),
|
||||
is_readonly=DATA.get("READONLY_MODE", False),
|
||||
username=current_user.get_id() if current_user.is_authenticated else "",
|
||||
)
|
||||
|
||||
|
|
@ -244,14 +179,14 @@ def set_security_headers(response):
|
|||
|
||||
@login_manager.user_loader
|
||||
def load_user(username):
|
||||
ui_user = app.db.get_ui_user(username=username)
|
||||
ui_user = DB.get_ui_user(username=username)
|
||||
if not ui_user:
|
||||
app.logger.warning(f"Couldn't get the user {username} from the database.")
|
||||
LOGGER.warning(f"Couldn't get the user {username} from the database.")
|
||||
return None
|
||||
|
||||
ui_user.list_roles = [role.role_name for role in ui_user.roles]
|
||||
for role in ui_user.list_roles:
|
||||
ui_user.list_permissions.extend(app.db.get_ui_role_permissions(role))
|
||||
ui_user.list_permissions.extend(DB.get_ui_role_permissions(role))
|
||||
|
||||
if ui_user.totp_secret:
|
||||
ui_user.list_recovery_codes = [recovery_code.code for recovery_code in ui_user.recovery_codes]
|
||||
|
|
@ -285,7 +220,7 @@ def handle_csrf_error(_):
|
|||
:param e: The exception object
|
||||
:return: A template with the error message and a 401 status code.
|
||||
"""
|
||||
app.logger.error(f"CSRF token is missing or invalid for {request.path} by {current_user.get_id()}")
|
||||
LOGGER.error(f"CSRF token is missing or invalid for {request.path} by {current_user.get_id()}")
|
||||
session.clear()
|
||||
logout_user()
|
||||
flash("Wrong CSRF token !", "error")
|
||||
|
|
@ -296,8 +231,8 @@ def handle_csrf_error(_):
|
|||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
app.data.load_from_file()
|
||||
if app.data.get("SERVER_STOPPING", False):
|
||||
DATA.load_from_file()
|
||||
if DATA.get("SERVER_STOPPING", False):
|
||||
response = make_response(jsonify({"message": "Server is shutting down, try again later."}), 503)
|
||||
response.headers["Retry-After"] = 30 # Clients should retry after 30 seconds # type: ignore
|
||||
return response
|
||||
|
|
@ -306,51 +241,45 @@ def before_request():
|
|||
|
||||
if not request.path.startswith(("/css", "/images", "/js", "/json", "/webfonts")):
|
||||
if (
|
||||
app.db.database_uri
|
||||
and app.db.readonly
|
||||
DB.database_uri
|
||||
and DB.readonly
|
||||
and (
|
||||
datetime.now(timezone.utc) - datetime.fromisoformat(app.data.get("LAST_DATABASE_RETRY", "1970-01-01T00:00:00")).replace(tzinfo=timezone.utc)
|
||||
datetime.now(timezone.utc) - datetime.fromisoformat(DATA.get("LAST_DATABASE_RETRY", "1970-01-01T00:00:00")).replace(tzinfo=timezone.utc)
|
||||
> timedelta(minutes=1)
|
||||
)
|
||||
):
|
||||
try:
|
||||
app.db.retry_connection(pool_timeout=1)
|
||||
app.db.retry_connection(log=False)
|
||||
app.data["READONLY_MODE"] = False
|
||||
app.logger.info("The database is no longer read-only, defaulting to read-write mode")
|
||||
DB.retry_connection(pool_timeout=1)
|
||||
DB.retry_connection(log=False)
|
||||
DATA["READONLY_MODE"] = False
|
||||
LOGGER.info("The database is no longer read-only, defaulting to read-write mode")
|
||||
except BaseException:
|
||||
try:
|
||||
app.db.retry_connection(readonly=True, pool_timeout=1)
|
||||
app.db.retry_connection(readonly=True, log=False)
|
||||
DB.retry_connection(readonly=True, pool_timeout=1)
|
||||
DB.retry_connection(readonly=True, log=False)
|
||||
except BaseException:
|
||||
if app.db.database_uri_readonly:
|
||||
if DB.database_uri_readonly:
|
||||
with suppress(BaseException):
|
||||
app.db.retry_connection(fallback=True, pool_timeout=1)
|
||||
app.db.retry_connection(fallback=True, log=False)
|
||||
app.data["READONLY_MODE"] = True
|
||||
app.data["LAST_DATABASE_RETRY"] = (
|
||||
app.db.last_connection_retry.isoformat() if app.db.last_connection_retry else datetime.now(timezone.utc).isoformat()
|
||||
)
|
||||
elif not app.data.get("READONLY_MODE", False) and request.method == "POST" and not ("/totp" in request.path or "/login" in request.path):
|
||||
DB.retry_connection(fallback=True, pool_timeout=1)
|
||||
DB.retry_connection(fallback=True, log=False)
|
||||
DATA["READONLY_MODE"] = True
|
||||
DATA["LAST_DATABASE_RETRY"] = DB.last_connection_retry.isoformat() if DB.last_connection_retry else datetime.now(timezone.utc).isoformat()
|
||||
elif not DATA.get("READONLY_MODE", False) and request.method == "POST" and not ("/totp" in request.path or "/login" in request.path):
|
||||
try:
|
||||
app.db.test_write()
|
||||
app.data["READONLY_MODE"] = False
|
||||
DB.test_write()
|
||||
DATA["READONLY_MODE"] = False
|
||||
except BaseException:
|
||||
app.data["READONLY_MODE"] = True
|
||||
app.data["LAST_DATABASE_RETRY"] = (
|
||||
app.db.last_connection_retry.isoformat() if app.db.last_connection_retry else datetime.now(timezone.utc).isoformat()
|
||||
)
|
||||
DATA["READONLY_MODE"] = True
|
||||
DATA["LAST_DATABASE_RETRY"] = DB.last_connection_retry.isoformat() if DB.last_connection_retry else datetime.now(timezone.utc).isoformat()
|
||||
else:
|
||||
try:
|
||||
app.db.test_read()
|
||||
DB.test_read()
|
||||
except BaseException:
|
||||
app.data["LAST_DATABASE_RETRY"] = (
|
||||
app.db.last_connection_retry.isoformat() if app.db.last_connection_retry else datetime.now(timezone.utc).isoformat()
|
||||
)
|
||||
DATA["LAST_DATABASE_RETRY"] = DB.last_connection_retry.isoformat() if DB.last_connection_retry else datetime.now(timezone.utc).isoformat()
|
||||
|
||||
app.db.readonly = app.data.get("READONLY_MODE", False)
|
||||
DB.readonly = DATA.get("READONLY_MODE", False)
|
||||
|
||||
if app.db.readonly:
|
||||
if DB.readonly:
|
||||
flash("Database connection is in read-only mode : no modification possible.", "error")
|
||||
|
||||
if current_user.is_authenticated:
|
||||
|
|
@ -362,10 +291,10 @@ def before_request():
|
|||
return redirect(url_for("totp.totp_page", next=request.form.get("next")))
|
||||
passed = False
|
||||
elif current_user.last_login_ip != request.remote_addr:
|
||||
app.logger.warning(f"User {current_user.get_id()} tried to access his session with a different IP address.")
|
||||
LOGGER.warning(f"User {current_user.get_id()} tried to access his session with a different IP address.")
|
||||
passed = False
|
||||
elif session.get("user_agent") != request.headers.get("User-Agent"):
|
||||
app.logger.warning(f"User {current_user.get_id()} tried to access his session with a different User-Agent.")
|
||||
LOGGER.warning(f"User {current_user.get_id()} tried to access his session with a different User-Agent.")
|
||||
passed = False
|
||||
|
||||
if not passed:
|
||||
|
|
@ -377,7 +306,7 @@ def before_request():
|
|||
|
||||
@app.route("/", strict_slashes=False)
|
||||
def index():
|
||||
if app.db.get_ui_user():
|
||||
if DB.get_ui_user():
|
||||
if current_user.is_authenticated: # type: ignore
|
||||
return redirect(url_for("home.home_page"))
|
||||
return redirect(url_for("login.login_page"), 301)
|
||||
|
|
@ -399,23 +328,23 @@ def check():
|
|||
@app.route("/check_reloading")
|
||||
@login_required
|
||||
def check_reloading():
|
||||
app.data.load_from_file()
|
||||
DATA.load_from_file()
|
||||
|
||||
if not app.data.get("RELOADING", False) or app.data.get("LAST_RELOAD", 0) + 60 < time():
|
||||
if app.data.get("RELOADING", False):
|
||||
app.logger.warning("Reloading took too long, forcing the state to be reloaded")
|
||||
if not DATA.get("RELOADING", False) or DATA.get("LAST_RELOAD", 0) + 60 < time():
|
||||
if DATA.get("RELOADING", False):
|
||||
LOGGER.warning("Reloading took too long, forcing the state to be reloaded")
|
||||
flash("Forced the status to be reloaded", "error")
|
||||
app.data["RELOADING"] = False
|
||||
DATA["RELOADING"] = False
|
||||
|
||||
for f in app.data.get("TO_FLASH", []):
|
||||
for f in DATA.get("TO_FLASH", []):
|
||||
if f["type"] == "error":
|
||||
flash(f["content"], "error")
|
||||
else:
|
||||
flash(f["content"])
|
||||
|
||||
app.data["TO_FLASH"] = []
|
||||
DATA["TO_FLASH"] = []
|
||||
|
||||
return jsonify({"reloading": app.data.get("RELOADING", False)})
|
||||
return jsonify({"reloading": DATA.get("RELOADING", False)})
|
||||
|
||||
|
||||
BLUEPRINTS = (bans, cache, configs, global_config, home, instances, jobs, modes, login, logout, logs, plugins, profile, reports, services, setup, totp)
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@ from json import dumps, loads as json_loads
|
|||
from math import floor
|
||||
from time import time
|
||||
|
||||
from flask import Blueprint, current_app, flash, redirect, render_template, request, url_for
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
from redis import Redis, Sentinel
|
||||
|
||||
from builder.bans import bans_builder # type: ignore
|
||||
|
||||
from dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DB
|
||||
from utils import LOGGER
|
||||
|
||||
from pages.utils import get_remain, handle_error, verify_data_in_form
|
||||
|
||||
bans = Blueprint("bans", __name__)
|
||||
|
|
@ -20,7 +23,7 @@ bans = Blueprint("bans", __name__)
|
|||
def bans_page():
|
||||
if request.method == "POST":
|
||||
|
||||
if current_app.db.readonly:
|
||||
if DB.readonly:
|
||||
return handle_error("Database is in read-only mode", "bans")
|
||||
|
||||
# Check variables
|
||||
|
|
@ -28,7 +31,7 @@ def bans_page():
|
|||
verify_data_in_form(data={"data": None}, err_message="Missing data parameter on /bans.", redirect_url="bans")
|
||||
|
||||
redis_client = None
|
||||
db_config = current_app.bw_config.get_config(
|
||||
db_config = BW_CONFIG.get_config(
|
||||
global_only=True,
|
||||
methods=False,
|
||||
filtered_settings=(
|
||||
|
|
@ -131,7 +134,7 @@ def bans_page():
|
|||
unban = json_loads(unban.replace('"', '"').replace("'", '"'))
|
||||
except BaseException:
|
||||
flash(f"Invalid unban: {unban}, skipping it ...", "error")
|
||||
current_app.logger.exception(f"Couldn't unban {unban['ip']}")
|
||||
LOGGER.exception(f"Couldn't unban {unban['ip']}")
|
||||
continue
|
||||
|
||||
if "ip" not in unban:
|
||||
|
|
@ -142,7 +145,7 @@ def bans_page():
|
|||
if not redis_client.delete(f"bans_ip_{unban['ip']}"):
|
||||
flash(f"Couldn't unban {unban['ip']} on redis", "error")
|
||||
|
||||
resp = current_app.bw_instances_utils.unban(unban["ip"])
|
||||
resp = BW_INSTANCES_UTILS.unban(unban["ip"])
|
||||
if resp:
|
||||
flash(f"Couldn't unban {unban['ip']} on the following instances: {', '.join(resp)}", "error")
|
||||
else:
|
||||
|
|
@ -173,7 +176,7 @@ def bans_page():
|
|||
flash(f"Couldn't ban {ban['ip']} on redis", "error")
|
||||
redis_client.expire(f"bans_ip_{ban['ip']}", int(ban_end))
|
||||
|
||||
resp = current_app.bw_instances_utils.ban(ban["ip"], ban_end, reason)
|
||||
resp = BW_INSTANCES_UTILS.ban(ban["ip"], ban_end, reason)
|
||||
if resp:
|
||||
flash(f"Couldn't ban {ban['ip']} on the following instances: {', '.join(resp)}", "error")
|
||||
else:
|
||||
|
|
@ -190,7 +193,7 @@ def bans_page():
|
|||
continue
|
||||
exp = redis_client.ttl(key)
|
||||
bans.append({"ip": ip, "exp": exp} | json_loads(data)) # type: ignore
|
||||
instance_bans = current_app.bw_instances_utils.get_bans()
|
||||
instance_bans = BW_INSTANCES_UTILS.get_bans()
|
||||
|
||||
# Prepare data
|
||||
timestamp_now = time()
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
from os.path import join, sep
|
||||
|
||||
from flask import Blueprint, current_app, render_template
|
||||
from flask import Blueprint, render_template
|
||||
from flask_login import login_required
|
||||
|
||||
from dependencies import BW_CONFIG, DB
|
||||
from utils import path_to_dict
|
||||
|
||||
|
||||
|
|
@ -18,10 +19,8 @@ def cache_page(): # TODO: refactor this function
|
|||
path_to_dict(
|
||||
join(sep, "var", "cache", "bunkerweb"),
|
||||
is_cache=True,
|
||||
db_data=current_app.db.get_jobs_cache_files(),
|
||||
services=current_app.bw_config.get_config(global_only=True, methods=False, filtered_settings=("SERVER_NAME",))
|
||||
.get("SERVER_NAME", "")
|
||||
.split(" "),
|
||||
db_data=DB.get_jobs_cache_files(),
|
||||
services=BW_CONFIG.get_config(global_only=True, methods=False, filtered_settings=("SERVER_NAME",)).get("SERVER_NAME", "").split(" "),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ from copy import deepcopy
|
|||
from os.path import join, sep
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from flask import Blueprint, current_app, flash, redirect, render_template, request, url_for
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from utils import path_to_dict
|
||||
from dependencies import BW_CONFIG, DATA, DB
|
||||
from utils import LOGGER, PLUGIN_NAME_RX, path_to_dict
|
||||
|
||||
from pages.utils import handle_error, verify_data_in_form
|
||||
|
||||
|
||||
|
|
@ -15,10 +17,10 @@ configs = Blueprint("configs", __name__)
|
|||
@configs.route("/configs", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def configs_page(): # TODO: refactor this function
|
||||
db_configs = current_app.db.get_custom_configs()
|
||||
db_configs = DB.get_custom_configs()
|
||||
|
||||
if request.method == "POST":
|
||||
if current_app.db.readonly:
|
||||
if DB.readonly:
|
||||
return handle_error("Database is in read-only mode", "configs")
|
||||
|
||||
operation = ""
|
||||
|
|
@ -39,10 +41,10 @@ def configs_page(): # TODO: refactor this function
|
|||
|
||||
# TODO: revamp this to use a path but a form to edit the content
|
||||
|
||||
operation = current_app.bw_custom_configs.check_path(variables["path"])
|
||||
# operation = BW_CUSTOM_CONFIGS.check_path(variables["path"])
|
||||
|
||||
if operation:
|
||||
return handle_error(operation, "configs", True)
|
||||
# if operation:
|
||||
# return handle_error(operation, "configs", True)
|
||||
|
||||
old_name = variables.get("old_name", "").replace(".conf", "")
|
||||
name = variables.get("name", old_name).replace(".conf", "")
|
||||
|
|
@ -69,7 +71,7 @@ def configs_page(): # TODO: refactor this function
|
|||
|
||||
# New or edit a config
|
||||
if request.form["operation"] in ("new", "edit"):
|
||||
if not current_app.bw_custom_configs.check_name(name):
|
||||
if not PLUGIN_NAME_RX.match(name):
|
||||
return handle_error(
|
||||
f"Invalid {variables['type']} name. (Can only contain numbers, letters, underscores, dots and hyphens (min 4 characters and max 64))",
|
||||
"configs",
|
||||
|
|
@ -107,12 +109,12 @@ def configs_page(): # TODO: refactor this function
|
|||
del db_configs[index]
|
||||
operation = f"Deleted config {name}{f' for service {service_id}' if service_id else ''}"
|
||||
|
||||
error = current_app.db.save_custom_configs([config for config in db_configs if config["method"] == "ui"], "ui")
|
||||
error = DB.save_custom_configs([config for config in db_configs if config["method"] == "ui"], "ui")
|
||||
if error:
|
||||
current_app.logger.error(f"Could not save custom configs: {error}")
|
||||
LOGGER.error(f"Could not save custom configs: {error}")
|
||||
return handle_error("Couldn't save custom configs", "configs", True)
|
||||
|
||||
current_app.data["CONFIG_CHANGED"] = True
|
||||
DATA["CONFIG_CHANGED"] = True
|
||||
|
||||
flash(operation)
|
||||
|
||||
|
|
@ -124,9 +126,7 @@ def configs_page(): # TODO: refactor this function
|
|||
path_to_dict(
|
||||
join(sep, "etc", "bunkerweb", "configs"),
|
||||
db_data=db_configs,
|
||||
services=current_app.bw_config.get_config(global_only=True, methods=False, filtered_settings=("SERVER_NAME",))
|
||||
.get("SERVER_NAME", "")
|
||||
.split(" "),
|
||||
services=BW_CONFIG.get_config(global_only=True, methods=False, filtered_settings=("SERVER_NAME",)).get("SERVER_NAME", "").split(" "),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ from json import dumps
|
|||
from threading import Thread
|
||||
from time import time
|
||||
|
||||
from flask import Blueprint, current_app, flash, redirect, render_template, request, url_for
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from builder.global_config import global_config_builder # type: ignore
|
||||
|
||||
from dependencies import BW_CONFIG, DATA, DB
|
||||
|
||||
from pages.utils import handle_error, manage_bunkerweb, wait_applying
|
||||
|
||||
|
||||
|
|
@ -19,7 +21,7 @@ global_config = Blueprint("global_config", __name__)
|
|||
@login_required
|
||||
def global_config_page():
|
||||
if request.method == "POST":
|
||||
if current_app.db.readonly:
|
||||
if DB.readonly:
|
||||
return handle_error("Database is in read-only mode", "global_config")
|
||||
|
||||
# Check variables
|
||||
|
|
@ -27,7 +29,7 @@ def global_config_page():
|
|||
del variables["csrf_token"]
|
||||
|
||||
# Edit check fields and remove already existing ones
|
||||
config = current_app.db.get_config(methods=True, with_drafts=True)
|
||||
config = DB.get_config(methods=True, with_drafts=True)
|
||||
services = config["SERVER_NAME"]["value"].split(" ")
|
||||
for variable, value in variables.copy().items():
|
||||
setting = config.get(variable, {"value": None, "global": True})
|
||||
|
|
@ -35,7 +37,7 @@ def global_config_page():
|
|||
del variables[variable]
|
||||
continue
|
||||
|
||||
variables = current_app.bw_config.check_variables(variables, config)
|
||||
variables = BW_CONFIG.check_variables(variables, config)
|
||||
|
||||
if not variables:
|
||||
return handle_error("The global configuration was not edited because no values were changed.", "global_config", True)
|
||||
|
|
@ -46,7 +48,7 @@ def global_config_page():
|
|||
if setting and setting["global"] and (setting["value"] != value or setting["value"] == config.get(variable, {"value": None})["value"]):
|
||||
variables[f"{service}_{variable}"] = value
|
||||
|
||||
db_metadata = current_app.db.get_metadata()
|
||||
db_metadata = DB.get_metadata()
|
||||
|
||||
def update_global_config(threaded: bool = False):
|
||||
wait_applying()
|
||||
|
|
@ -54,20 +56,20 @@ def global_config_page():
|
|||
manage_bunkerweb("global_config", variables, threaded=threaded)
|
||||
|
||||
if "PRO_LICENSE_KEY" in variables:
|
||||
current_app.data["PRO_LOADING"] = True
|
||||
DATA["PRO_LOADING"] = True
|
||||
|
||||
if any(
|
||||
v
|
||||
for k, v in db_metadata.items()
|
||||
if k in ("custom_configs_changed", "external_plugins_changed", "pro_plugins_changed", "plugins_config_changed", "instances_changed")
|
||||
):
|
||||
current_app.data["RELOADING"] = True
|
||||
current_app.data["LAST_RELOAD"] = time()
|
||||
DATA["RELOADING"] = True
|
||||
DATA["LAST_RELOAD"] = time()
|
||||
Thread(target=update_global_config, args=(True,)).start()
|
||||
else:
|
||||
update_global_config()
|
||||
|
||||
current_app.data["CONFIG_CHANGED"] = True
|
||||
DATA["CONFIG_CHANGED"] = True
|
||||
|
||||
with suppress(BaseException):
|
||||
if config["PRO_LICENSE_KEY"]["value"] != variables["PRO_LICENSE_KEY"]:
|
||||
|
|
@ -81,7 +83,7 @@ def global_config_page():
|
|||
)
|
||||
)
|
||||
|
||||
global_config = current_app.bw_config.get_config(global_only=True, methods=True)
|
||||
plugins = current_app.bw_config.get_plugins()
|
||||
global_config = BW_CONFIG.get_config(global_only=True, methods=True)
|
||||
plugins = BW_CONFIG.get_plugins()
|
||||
builder = global_config_builder({}, plugins, global_config)
|
||||
return render_template("global-config.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from base64 import b64encode
|
|||
from json import dumps
|
||||
from os.path import basename
|
||||
|
||||
from flask import Blueprint, current_app, render_template
|
||||
from flask import Blueprint, render_template
|
||||
from flask_login import login_required
|
||||
from requests import get
|
||||
|
||||
|
|
@ -10,6 +10,7 @@ from common_utils import get_version # type: ignore
|
|||
|
||||
from builder.home import home_builder # type: ignore
|
||||
|
||||
from dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DB
|
||||
|
||||
home = Blueprint("home", __name__)
|
||||
|
||||
|
|
@ -37,8 +38,8 @@ def home_page():
|
|||
if r and r.status_code == 200:
|
||||
remote_version = basename(r.url).strip().replace("v", "")
|
||||
|
||||
config = current_app.bw_config.get_config(with_drafts=True, filtered_settings=("SERVER_NAME",))
|
||||
instances = current_app.bw_instances_utils.get_instances()
|
||||
config = BW_CONFIG.get_config(with_drafts=True, filtered_settings=("SERVER_NAME",))
|
||||
instances = BW_INSTANCES_UTILS.get_instances()
|
||||
|
||||
instance_health_count = 0
|
||||
|
||||
|
|
@ -62,7 +63,7 @@ def home_page():
|
|||
services_autoconf_count += 1
|
||||
services += 1
|
||||
|
||||
metadata = current_app.db.get_metadata()
|
||||
metadata = DB.get_metadata()
|
||||
|
||||
data = {
|
||||
"check_version": not remote_version or get_version() == remote_version,
|
||||
|
|
@ -78,8 +79,8 @@ def home_page():
|
|||
"pro_status": metadata["pro_status"],
|
||||
"pro_services": metadata["pro_services"],
|
||||
"pro_overlapped": metadata["pro_overlapped"],
|
||||
"plugins_number": len(current_app.bw_config.get_plugins()),
|
||||
"plugins_errors": current_app.db.get_plugins_errors(),
|
||||
"plugins_number": len(BW_CONFIG.get_plugins()),
|
||||
"plugins_errors": DB.get_plugins_errors(),
|
||||
}
|
||||
|
||||
builder = home_builder(data)
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ from json import dumps
|
|||
from threading import Thread
|
||||
from time import time
|
||||
from typing import Literal
|
||||
from flask import Blueprint, current_app, redirect, render_template, request, url_for
|
||||
from flask import Blueprint, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from builder.instances import instances_builder # type: ignore
|
||||
|
||||
from dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DATA, DB
|
||||
|
||||
from pages.utils import handle_error, manage_bunkerweb, verify_data_in_form
|
||||
|
||||
|
||||
|
|
@ -22,7 +24,7 @@ def instances_page():
|
|||
instances_methods = set()
|
||||
instances_healths = set()
|
||||
|
||||
for instance in current_app.bw_instances_utils.get_instances():
|
||||
for instance in BW_INSTANCES_UTILS.get_instances():
|
||||
instances.append(
|
||||
{
|
||||
"hostname": instance.hostname,
|
||||
|
|
@ -65,7 +67,7 @@ def instances_new():
|
|||
next=True,
|
||||
)
|
||||
|
||||
db_config = current_app.bw_config.get_config(global_only=True, methods=False, filtered_settings=("API_HTTP_PORT", "API_SERVER_NAME"))
|
||||
db_config = BW_CONFIG.get_config(global_only=True, methods=False, filtered_settings=("API_HTTP_PORT", "API_SERVER_NAME"))
|
||||
|
||||
instance = {
|
||||
"hostname": request.form["instance_hostname"].replace("http://", "").replace("https://", ""),
|
||||
|
|
@ -75,11 +77,11 @@ def instances_new():
|
|||
"method": "ui",
|
||||
}
|
||||
|
||||
for db_instance in current_app.bw_instances_utils.get_instances():
|
||||
for db_instance in BW_INSTANCES_UTILS.get_instances():
|
||||
if db_instance.hostname == instance["hostname"]:
|
||||
return handle_error(f"The hostname {instance['hostname']} is already in use.", "instances", True)
|
||||
|
||||
ret = current_app.db.add_instance(**instance)
|
||||
ret = DB.add_instance(**instance)
|
||||
if ret:
|
||||
return handle_error(f"Couldn't create the instance in the database: {ret}", "instances", True)
|
||||
|
||||
|
|
@ -97,7 +99,7 @@ def instances_delete(instance_hostname: str):
|
|||
)
|
||||
|
||||
delete_instance = None
|
||||
for instance in current_app.bw_instances_utils.get_instances():
|
||||
for instance in BW_INSTANCES_UTILS.get_instances():
|
||||
if instance.hostname == instance_hostname:
|
||||
delete_instance = instance
|
||||
break
|
||||
|
|
@ -107,7 +109,7 @@ def instances_delete(instance_hostname: str):
|
|||
if delete_instance.method != "ui":
|
||||
return handle_error(f"Instance {instance_hostname} is not a UI instance.", "instances", True)
|
||||
|
||||
ret = current_app.db.delete_instance(instance_hostname)
|
||||
ret = DB.delete_instance(instance_hostname)
|
||||
if ret:
|
||||
return handle_error(f"Couldn't delete the instance in the database: {ret}", "instances", True)
|
||||
|
||||
|
|
@ -124,8 +126,8 @@ def instances_action(action: Literal["ping", "reload", "stop"]): # TODO: see if
|
|||
next=True,
|
||||
)
|
||||
|
||||
current_app.data["RELOADING"] = True
|
||||
current_app.data["LAST_RELOAD"] = time()
|
||||
DATA["RELOADING"] = True
|
||||
DATA["LAST_RELOAD"] = time()
|
||||
Thread(
|
||||
target=manage_bunkerweb,
|
||||
name=f"Reloading instance {request.form['instance_hostname']}",
|
||||
|
|
|
|||
|
|
@ -2,19 +2,21 @@ from base64 import b64encode
|
|||
from io import BytesIO
|
||||
from json import dumps
|
||||
|
||||
from flask import Blueprint, current_app, jsonify, render_template, request, send_file
|
||||
from flask import Blueprint, jsonify, render_template, request, send_file
|
||||
from flask_login import login_required
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from builder.jobs import jobs_builder # type: ignore
|
||||
|
||||
from dependencies import DB
|
||||
|
||||
jobs = Blueprint("jobs", __name__)
|
||||
|
||||
|
||||
@jobs.route("/jobs", methods=["GET"])
|
||||
@login_required
|
||||
def jobs_page():
|
||||
builder = jobs_builder(current_app.db.get_jobs())
|
||||
builder = jobs_builder(DB.get_jobs())
|
||||
return render_template("jobs.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
|
||||
|
||||
|
|
@ -29,7 +31,7 @@ def jobs_download():
|
|||
if not plugin_id or not job_name or not file_name:
|
||||
return jsonify({"status": "ko", "message": "plugin_id, job_name and file_name are required"}), 422
|
||||
|
||||
cache_file = current_app.db.get_job_cache_file(job_name, file_name, service_id=service_id, plugin_id=plugin_id)
|
||||
cache_file = DB.get_job_cache_file(job_name, file_name, service_id=service_id, plugin_id=plugin_id)
|
||||
|
||||
if not cache_file:
|
||||
return jsonify({"status": "ko", "message": "file not found"}), 404
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
from datetime import datetime, timezone
|
||||
|
||||
from flask import Blueprint, current_app, flash, redirect, render_template, request, session, url_for
|
||||
from flask import Blueprint, flash, redirect, render_template, request, session, url_for
|
||||
from flask_login import current_user, login_user
|
||||
|
||||
from dependencies import DB
|
||||
from utils import LOGGER
|
||||
|
||||
login = Blueprint("login", __name__)
|
||||
|
||||
|
||||
@login.route("/login", methods=["GET", "POST"])
|
||||
def login_page():
|
||||
admin_user = current_app.db.get_ui_user()
|
||||
admin_user = DB.get_ui_user()
|
||||
if not admin_user:
|
||||
return redirect(url_for("setup.setup_page"))
|
||||
elif current_user.is_authenticated: # type: ignore
|
||||
|
|
@ -17,9 +19,9 @@ def login_page():
|
|||
|
||||
fail = False
|
||||
if request.method == "POST" and "username" in request.form and "password" in request.form:
|
||||
current_app.logger.warning(f"Login attempt from {request.remote_addr} with username \"{request.form['username']}\"")
|
||||
LOGGER.warning(f"Login attempt from {request.remote_addr} with username \"{request.form['username']}\"")
|
||||
|
||||
ui_user = current_app.db.get_ui_user(username=request.form["username"])
|
||||
ui_user = DB.get_ui_user(username=request.form["username"])
|
||||
if ui_user and ui_user.username == request.form["username"] and ui_user.check_password(request.form["password"]):
|
||||
# log the user in
|
||||
session["user_agent"] = request.headers.get("User-Agent")
|
||||
|
|
@ -29,13 +31,13 @@ def login_page():
|
|||
ui_user.last_login_ip = request.remote_addr
|
||||
ui_user.login_count += 1
|
||||
|
||||
current_app.db.mark_ui_user_login(ui_user.username, ui_user.last_login_at, ui_user.last_login_ip)
|
||||
DB.mark_ui_user_login(ui_user.username, ui_user.last_login_at, ui_user.last_login_ip)
|
||||
|
||||
if not login_user(ui_user, remember=request.form.get("remember") == "on"):
|
||||
flash("Couldn't log you in, please try again", "error")
|
||||
return (render_template("login.html", error="Couldn't log you in, please try again"),)
|
||||
|
||||
current_app.logger.info(
|
||||
LOGGER.info(
|
||||
f"User {ui_user.username} logged in successfully for the {str(ui_user.login_count) + ('th' if 10 <= ui_user.login_count % 100 <= 20 else {1: 'st', 2: 'nd', 3: 'rd'}.get(ui_user.login_count % 10, 'th'))} time"
|
||||
+ (" with remember me" if request.form.get("remember") == "on" else "")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
from base64 import b64encode
|
||||
from json import dumps
|
||||
|
||||
from flask import Blueprint, current_app, redirect, render_template, request, url_for
|
||||
from flask import Blueprint, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from builder.advanced_mode import advanced_mode_builder # type: ignore
|
||||
from builder.easy_mode import easy_mode_builder # type: ignore
|
||||
from builder.raw_mode import raw_mode_builder # type: ignore
|
||||
|
||||
from pages.utils import get_service_data, handle_error, update_service
|
||||
from dependencies import BW_CONFIG, DB
|
||||
|
||||
from pages.utils import get_service_data, handle_error, update_service
|
||||
|
||||
modes = Blueprint("modes", __name__)
|
||||
|
||||
|
|
@ -18,7 +19,7 @@ modes = Blueprint("modes", __name__)
|
|||
@login_required
|
||||
def services_modes():
|
||||
if request.method == "POST":
|
||||
if current_app.db.readonly:
|
||||
if DB.readonly:
|
||||
return handle_error("Database is in read-only mode", "services")
|
||||
|
||||
config, variables, format_configs, server_name, old_server_name, operation, is_draft, was_draft, is_draft_unchanged, mode = get_service_data("modes")
|
||||
|
|
@ -37,17 +38,17 @@ def services_modes():
|
|||
|
||||
mode = request.args.get("mode")
|
||||
service_name = request.args.get("service_name")
|
||||
total_config = current_app.db.get_config(methods=True, with_drafts=True)
|
||||
total_config = DB.get_config(methods=True, with_drafts=True)
|
||||
service_names = total_config["SERVER_NAME"]["value"].split(" ")
|
||||
|
||||
if service_name and service_name not in service_names:
|
||||
return handle_error("Service name not found to access advanced mode.", "services")
|
||||
|
||||
global_config = current_app.bw_config.get_config(global_only=True, methods=True)
|
||||
plugins = current_app.bw_config.get_plugins()
|
||||
global_config = BW_CONFIG.get_config(global_only=True, methods=True)
|
||||
plugins = BW_CONFIG.get_plugins()
|
||||
|
||||
builder = None
|
||||
templates_db = current_app.db.get_templates()
|
||||
templates_db = DB.get_templates()
|
||||
|
||||
if mode == "raw":
|
||||
builder = raw_mode_builder(templates_db, plugins, global_config, total_config, service_name or "new", not service_name)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
from base64 import b64encode
|
||||
from copy import deepcopy
|
||||
from importlib.machinery import SourceFileLoader
|
||||
from io import BytesIO
|
||||
from json import JSONDecodeError, dumps, loads as json_loads
|
||||
from os import listdir
|
||||
from os.path import basename, dirname, isabs, join, sep
|
||||
from pathlib import Path
|
||||
from shutil import move, rmtree
|
||||
from sys import path as sys_path
|
||||
from tarfile import CompressionError, HeaderError, ReadError, TarError, open as tar_open
|
||||
from threading import Thread
|
||||
from time import time
|
||||
from typing import Optional, Union
|
||||
from uuid import uuid4
|
||||
from zipfile import BadZipFile, ZipFile
|
||||
|
||||
|
|
@ -21,283 +24,320 @@ from common_utils import bytes_hash # type: ignore
|
|||
|
||||
from builder.plugins import plugins_builder # type: ignore
|
||||
|
||||
from pages.utils import PLUGIN_ID_RX, PLUGIN_KEYS, TMP_DIR, error_message, handle_error, run_action, verify_data_in_form, wait_applying
|
||||
from dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DATA, DB
|
||||
from utils import LOGGER, PLUGIN_NAME_RX, TMP_DIR
|
||||
|
||||
from pages.utils import PLUGIN_ID_RX, PLUGIN_KEYS, error_message, handle_error, verify_data_in_form, wait_applying
|
||||
|
||||
|
||||
plugins = Blueprint("plugins", __name__)
|
||||
|
||||
|
||||
def run_action(plugin: str, function_name: str = "", *, tmp_dir: Optional[Path] = None) -> Union[dict, Response]:
|
||||
message = ""
|
||||
if not tmp_dir:
|
||||
page = DB.get_plugin_page(plugin)
|
||||
|
||||
if not page:
|
||||
return {"status": "ko", "code": 404, "message": "The plugin does not have a page"}
|
||||
|
||||
try:
|
||||
# Try to import the plugin's custom page
|
||||
tmp_dir = TMP_DIR.joinpath("ui", "action", str(uuid4()))
|
||||
tmp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with tar_open(fileobj=BytesIO(page), mode="r:gz") as tar:
|
||||
tar.extractall(tmp_dir)
|
||||
|
||||
tmp_dir = tmp_dir.joinpath("ui")
|
||||
except BaseException as e:
|
||||
LOGGER.error(f"An error occurred while extracting the plugin: {e}")
|
||||
return {"status": "ko", "code": 500, "message": "An error occurred while extracting the plugin, see logs for more details"}
|
||||
|
||||
try:
|
||||
action_file = tmp_dir.joinpath("actions.py")
|
||||
if not action_file.is_file():
|
||||
return {"status": "ko", "code": 404, "message": "The plugin does not have an action file"}
|
||||
|
||||
sys_path.append(tmp_dir.as_posix())
|
||||
loader = SourceFileLoader("actions", action_file.as_posix())
|
||||
actions = loader.load_module()
|
||||
except BaseException as e:
|
||||
sys_path.pop()
|
||||
if function_name != "pre_render":
|
||||
rmtree(tmp_dir, ignore_errors=True)
|
||||
|
||||
LOGGER.error(f"An error occurred while importing the plugin: {e}")
|
||||
return {"status": "ko", "code": 500, "message": "An error occurred while importing the plugin, see logs for more details"}
|
||||
|
||||
exception = None
|
||||
res = None
|
||||
message = None
|
||||
|
||||
try:
|
||||
# Try to get the custom plugin custom function and call it
|
||||
method = getattr(actions, function_name or plugin)
|
||||
queries = request.args.to_dict()
|
||||
try:
|
||||
data = request.json or {}
|
||||
except BaseException:
|
||||
data = {}
|
||||
|
||||
res = method(app=current_app, db=DB, instances_utils=BW_INSTANCES_UTILS, args=queries, data=data)
|
||||
except AttributeError as e:
|
||||
if function_name == "pre_render":
|
||||
sys_path.pop()
|
||||
return {"status": "ok", "code": 200, "message": "The plugin does not have a pre_render method"}
|
||||
|
||||
message = "The plugin does not have a method"
|
||||
exception = e
|
||||
except BaseException as e:
|
||||
message = "An error occurred while executing the plugin"
|
||||
exception = e
|
||||
finally:
|
||||
sys_path.pop()
|
||||
|
||||
if function_name != "pre_render":
|
||||
rmtree(tmp_dir, ignore_errors=True)
|
||||
|
||||
if message:
|
||||
LOGGER.error(message + (f": {exception}" if exception else ""))
|
||||
if message or not isinstance(res, dict) and not res:
|
||||
return {
|
||||
"status": "ko",
|
||||
"code": 500,
|
||||
"message": message + ", see logs for more details" if message else "The plugin did not return a valid response",
|
||||
}
|
||||
|
||||
if isinstance(res, Response):
|
||||
return res
|
||||
|
||||
return {"status": "ok", "code": 200, "data": res}
|
||||
|
||||
|
||||
@plugins.route("/plugins", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def plugins_page():
|
||||
tmp_ui_path = TMP_DIR.joinpath("ui")
|
||||
|
||||
if request.method == "POST":
|
||||
if current_app.db.readonly:
|
||||
if DB.readonly:
|
||||
return handle_error("Database is in read-only mode", "plugins")
|
||||
|
||||
verify_data_in_form(
|
||||
data={"operation": ("delete"), "type": None},
|
||||
err_message="Missing type parameter for operation delete on /plugins.",
|
||||
data={"csrf_token": None},
|
||||
err_message="Missing csrf_token parameter on /plugins.",
|
||||
redirect_url="plugins",
|
||||
next=True,
|
||||
)
|
||||
|
||||
error = 0
|
||||
# Delete plugin
|
||||
if request.form["operation"] == "delete":
|
||||
# Upload plugins
|
||||
if not tmp_ui_path.exists() or not listdir(str(tmp_ui_path)):
|
||||
return handle_error("Please upload new plugins to reload plugins", "plugins", True)
|
||||
|
||||
# Check variables
|
||||
variables = deepcopy(request.form.to_dict())
|
||||
del variables["csrf_token"]
|
||||
errors = 0
|
||||
files_count = 0
|
||||
new_plugins = []
|
||||
new_plugins_ids = []
|
||||
|
||||
if variables["type"] in ("core", "pro"):
|
||||
return handle_error(f"Can't delete {variables['type']} plugin {variables['name']}", "plugins", True)
|
||||
for file in listdir(str(tmp_ui_path)):
|
||||
if not tmp_ui_path.joinpath(file).is_file():
|
||||
continue
|
||||
|
||||
db_metadata = current_app.db.get_metadata()
|
||||
files_count += 1
|
||||
folder_name = ""
|
||||
temp_folder_name = file.split(".")[0]
|
||||
temp_folder_path = tmp_ui_path.joinpath(temp_folder_name)
|
||||
is_dir = False
|
||||
|
||||
def update_plugins(threaded: bool = False): # type: ignore
|
||||
wait_applying()
|
||||
|
||||
plugins = current_app.bw_config.get_plugins(_type="external", with_data=True)
|
||||
for x, plugin in enumerate(plugins):
|
||||
if plugin["id"] == variables["name"]:
|
||||
del plugins[x]
|
||||
|
||||
err = current_app.db.update_external_plugins(plugins)
|
||||
if err:
|
||||
message = f"Couldn't update external plugins to database: {err}"
|
||||
if threaded:
|
||||
current_app.data["TO_FLASH"].append({"content": message, "type": "error"})
|
||||
else:
|
||||
error_message(message)
|
||||
else:
|
||||
message = f"Deleted plugin {variables['name']} successfully"
|
||||
if threaded:
|
||||
current_app.data["TO_FLASH"].append({"content": message, "type": "success"})
|
||||
else:
|
||||
flash(message)
|
||||
|
||||
current_app.data["RELOADING"] = False
|
||||
|
||||
if any(
|
||||
v
|
||||
for k, v in db_metadata.items()
|
||||
if k in ("custom_configs_changed", "external_plugins_changed", "pro_plugins_changed", "plugins_config_changed", "instances_changed")
|
||||
):
|
||||
current_app.data["RELOADING"] = True
|
||||
current_app.data["LAST_RELOAD"] = time()
|
||||
|
||||
Thread(target=update_plugins, args=(True,)).start()
|
||||
else:
|
||||
update_plugins()
|
||||
else:
|
||||
# Upload plugins
|
||||
if not tmp_ui_path.exists() or not listdir(str(tmp_ui_path)):
|
||||
return handle_error("Please upload new plugins to reload plugins", "plugins", True)
|
||||
|
||||
errors = 0
|
||||
files_count = 0
|
||||
new_plugins = []
|
||||
new_plugins_ids = []
|
||||
|
||||
for file in listdir(str(tmp_ui_path)):
|
||||
if not tmp_ui_path.joinpath(file).is_file():
|
||||
continue
|
||||
|
||||
files_count += 1
|
||||
folder_name = ""
|
||||
temp_folder_name = file.split(".")[0]
|
||||
temp_folder_path = tmp_ui_path.joinpath(temp_folder_name)
|
||||
is_dir = False
|
||||
|
||||
try:
|
||||
if file.endswith(".zip"):
|
||||
try:
|
||||
with ZipFile(str(tmp_ui_path.joinpath(file))) as zip_file:
|
||||
try:
|
||||
zip_file.getinfo("plugin.json")
|
||||
except KeyError:
|
||||
is_dir = True
|
||||
zip_file.extractall(str(temp_folder_path))
|
||||
except BadZipFile:
|
||||
errors += 1
|
||||
error = 1
|
||||
message = f"{file} is not a valid zip file. ({folder_name or temp_folder_name})"
|
||||
current_app.logger.exception(message)
|
||||
flash(message, "error")
|
||||
else:
|
||||
try:
|
||||
with tar_open(str(tmp_ui_path.joinpath(file)), errorlevel=2) as tar_file:
|
||||
try:
|
||||
tar_file.getmember("plugin.json")
|
||||
except KeyError:
|
||||
is_dir = True
|
||||
try:
|
||||
# deepcode ignore TarSlip: We don't need to check for tar slip as we are checking the files when they are uploaded
|
||||
tar_file.extractall(str(temp_folder_path), filter="data")
|
||||
except TypeError:
|
||||
# deepcode ignore TarSlip: We don't need to check for tar slip as we are checking the files when they are uploaded
|
||||
tar_file.extractall(str(temp_folder_path))
|
||||
except ReadError:
|
||||
errors += 1
|
||||
error = 1
|
||||
message = f"Couldn't read file {file} ({folder_name or temp_folder_name})"
|
||||
current_app.logger.exception(message)
|
||||
flash(message, "error")
|
||||
except CompressionError:
|
||||
errors += 1
|
||||
error = 1
|
||||
message = f"{file} is not a valid tar file ({folder_name or temp_folder_name})"
|
||||
current_app.logger.exception(message)
|
||||
flash(message, "error")
|
||||
except HeaderError:
|
||||
errors += 1
|
||||
error = 1
|
||||
message = f"The file plugin.json in {file} is not valid ({folder_name or temp_folder_name})"
|
||||
current_app.logger.exception(message)
|
||||
flash(message, "error")
|
||||
|
||||
if is_dir:
|
||||
dirs = [d for d in listdir(str(temp_folder_path)) if temp_folder_path.joinpath(d).is_dir()]
|
||||
|
||||
if not dirs or len(dirs) > 1 or not temp_folder_path.joinpath(dirs[0], "plugin.json").is_file():
|
||||
raise KeyError
|
||||
|
||||
for file_name in listdir(str(temp_folder_path.joinpath(dirs[0]))):
|
||||
move(
|
||||
str(temp_folder_path.joinpath(dirs[0], file_name)),
|
||||
str(temp_folder_path.joinpath(file_name)),
|
||||
)
|
||||
rmtree(
|
||||
str(temp_folder_path.joinpath(dirs[0])),
|
||||
ignore_errors=True,
|
||||
)
|
||||
|
||||
plugin_file = json_loads(temp_folder_path.joinpath("plugin.json").read_text(encoding="utf-8"))
|
||||
|
||||
if not all(key in plugin_file.keys() for key in PLUGIN_KEYS):
|
||||
raise ValueError
|
||||
|
||||
folder_name = plugin_file["id"]
|
||||
|
||||
if not current_app.bw_custom_configs.check_name(folder_name):
|
||||
try:
|
||||
if file.endswith(".zip"):
|
||||
try:
|
||||
with ZipFile(str(tmp_ui_path.joinpath(file))) as zip_file:
|
||||
try:
|
||||
zip_file.getinfo("plugin.json")
|
||||
except KeyError:
|
||||
is_dir = True
|
||||
zip_file.extractall(str(temp_folder_path))
|
||||
except BadZipFile:
|
||||
errors += 1
|
||||
error = 1
|
||||
flash(
|
||||
f"Invalid plugin name for {temp_folder_name}. (Can only contain numbers, letters, underscores and hyphens (min 4 characters and max 64))",
|
||||
"error",
|
||||
)
|
||||
raise Exception
|
||||
|
||||
plugin_content = BytesIO()
|
||||
with tar_open(
|
||||
fileobj=plugin_content,
|
||||
mode="w:gz",
|
||||
compresslevel=9,
|
||||
) as tar:
|
||||
tar.add(
|
||||
str(temp_folder_path),
|
||||
arcname=temp_folder_name,
|
||||
recursive=True,
|
||||
)
|
||||
plugin_content.seek(0)
|
||||
value = plugin_content.getvalue()
|
||||
|
||||
new_plugins.append(
|
||||
plugin_file
|
||||
| {
|
||||
"type": "external",
|
||||
"page": "ui" in listdir(str(temp_folder_path)),
|
||||
"method": "ui",
|
||||
"data": value,
|
||||
"checksum": bytes_hash(value, algorithm="sha256"),
|
||||
}
|
||||
)
|
||||
new_plugins_ids.append(folder_name)
|
||||
except KeyError:
|
||||
errors += 1
|
||||
error = 1
|
||||
flash(
|
||||
f"{file} is not a valid plugin (plugin.json file is missing) ({folder_name or temp_folder_name})",
|
||||
"error",
|
||||
)
|
||||
except JSONDecodeError as e:
|
||||
errors += 1
|
||||
error = 1
|
||||
flash(
|
||||
f"The file plugin.json in {file} is not valid ({e.msg}: line {e.lineno} column {e.colno} (char {e.pos})) ({folder_name or temp_folder_name})",
|
||||
"error",
|
||||
)
|
||||
except ValueError:
|
||||
errors += 1
|
||||
error = 1
|
||||
flash(
|
||||
f"The file plugin.json is missing one or more of the following keys: <i>{', '.join(PLUGIN_KEYS)}</i> ({folder_name or temp_folder_name})",
|
||||
"error",
|
||||
)
|
||||
except FileExistsError:
|
||||
errors += 1
|
||||
error = 1
|
||||
flash(
|
||||
f"A plugin named {folder_name} already exists",
|
||||
"error",
|
||||
)
|
||||
except (TarError, OSError) as e:
|
||||
errors += 1
|
||||
error = 1
|
||||
flash(str(e), "error")
|
||||
except Exception as e:
|
||||
errors += 1
|
||||
error = 1
|
||||
flash(str(e), "error")
|
||||
finally:
|
||||
if error != 1:
|
||||
flash(f"Successfully created plugin: <b><i>{folder_name}</i></b>")
|
||||
|
||||
error = 0
|
||||
|
||||
if errors >= files_count:
|
||||
return redirect(url_for("loading", next=url_for("plugins.plugins_page")))
|
||||
|
||||
db_metadata = current_app.db.get_metadata()
|
||||
|
||||
def update_plugins(threaded: bool = False):
|
||||
wait_applying()
|
||||
|
||||
plugins = current_app.bw_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")
|
||||
del new_plugins[new_plugins_ids.index(plugin["id"])]
|
||||
|
||||
err = current_app.db.update_external_plugins(new_plugins, delete_missing=False)
|
||||
if err:
|
||||
message = f"Couldn't update external plugins to database: {err}"
|
||||
if threaded:
|
||||
current_app.data["TO_FLASH"].append({"content": message, "type": "error"})
|
||||
else:
|
||||
message = f"{file} is not a valid zip file. ({folder_name or temp_folder_name})"
|
||||
LOGGER.exception(message)
|
||||
flash(message, "error")
|
||||
else:
|
||||
message = "Plugins uploaded successfully"
|
||||
if threaded:
|
||||
current_app.data["TO_FLASH"].append({"content": message, "type": "success"})
|
||||
else:
|
||||
flash("Plugins uploaded successfully")
|
||||
try:
|
||||
with tar_open(str(tmp_ui_path.joinpath(file)), errorlevel=2) as tar_file:
|
||||
try:
|
||||
tar_file.getmember("plugin.json")
|
||||
except KeyError:
|
||||
is_dir = True
|
||||
try:
|
||||
# deepcode ignore TarSlip: We don't need to check for tar slip as we are checking the files when they are uploaded
|
||||
tar_file.extractall(str(temp_folder_path), filter="data")
|
||||
except TypeError:
|
||||
# deepcode ignore TarSlip: We don't need to check for tar slip as we are checking the files when they are uploaded
|
||||
tar_file.extractall(str(temp_folder_path))
|
||||
except ReadError:
|
||||
errors += 1
|
||||
error = 1
|
||||
message = f"Couldn't read file {file} ({folder_name or temp_folder_name})"
|
||||
LOGGER.exception(message)
|
||||
flash(message, "error")
|
||||
except CompressionError:
|
||||
errors += 1
|
||||
error = 1
|
||||
message = f"{file} is not a valid tar file ({folder_name or temp_folder_name})"
|
||||
LOGGER.exception(message)
|
||||
flash(message, "error")
|
||||
except HeaderError:
|
||||
errors += 1
|
||||
error = 1
|
||||
message = f"The file plugin.json in {file} is not valid ({folder_name or temp_folder_name})"
|
||||
LOGGER.exception(message)
|
||||
flash(message, "error")
|
||||
|
||||
current_app.data["RELOADING"] = False
|
||||
if is_dir:
|
||||
dirs = [d for d in listdir(str(temp_folder_path)) if temp_folder_path.joinpath(d).is_dir()]
|
||||
|
||||
if any(
|
||||
v
|
||||
for k, v in db_metadata.items()
|
||||
if k in ("custom_configs_changed", "external_plugins_changed", "pro_plugins_changed", "plugins_config_changed", "instances_changed")
|
||||
):
|
||||
current_app.data["RELOADING"] = True
|
||||
current_app.data["LAST_RELOAD"] = time()
|
||||
if not dirs or len(dirs) > 1 or not temp_folder_path.joinpath(dirs[0], "plugin.json").is_file():
|
||||
raise KeyError
|
||||
|
||||
Thread(target=update_plugins, args=(True,)).start()
|
||||
for file_name in listdir(str(temp_folder_path.joinpath(dirs[0]))):
|
||||
move(
|
||||
str(temp_folder_path.joinpath(dirs[0], file_name)),
|
||||
str(temp_folder_path.joinpath(file_name)),
|
||||
)
|
||||
rmtree(
|
||||
str(temp_folder_path.joinpath(dirs[0])),
|
||||
ignore_errors=True,
|
||||
)
|
||||
|
||||
plugin_file = json_loads(temp_folder_path.joinpath("plugin.json").read_text(encoding="utf-8"))
|
||||
|
||||
if not all(key in plugin_file.keys() for key in PLUGIN_KEYS):
|
||||
raise ValueError
|
||||
|
||||
folder_name = plugin_file["id"]
|
||||
|
||||
if not PLUGIN_NAME_RX.match(folder_name):
|
||||
errors += 1
|
||||
error = 1
|
||||
flash(
|
||||
f"Invalid plugin name for {temp_folder_name}. (Can only contain numbers, letters, underscores and hyphens (min 4 characters and max 64))",
|
||||
"error",
|
||||
)
|
||||
raise Exception
|
||||
|
||||
plugin_content = BytesIO()
|
||||
with tar_open(
|
||||
fileobj=plugin_content,
|
||||
mode="w:gz",
|
||||
compresslevel=9,
|
||||
) as tar:
|
||||
tar.add(
|
||||
str(temp_folder_path),
|
||||
arcname=temp_folder_name,
|
||||
recursive=True,
|
||||
)
|
||||
plugin_content.seek(0)
|
||||
value = plugin_content.getvalue()
|
||||
|
||||
new_plugins.append(
|
||||
plugin_file
|
||||
| {
|
||||
"type": "external",
|
||||
"page": "ui" in listdir(str(temp_folder_path)),
|
||||
"method": "ui",
|
||||
"data": value,
|
||||
"checksum": bytes_hash(value, algorithm="sha256"),
|
||||
}
|
||||
)
|
||||
new_plugins_ids.append(folder_name)
|
||||
except KeyError:
|
||||
errors += 1
|
||||
error = 1
|
||||
flash(
|
||||
f"{file} is not a valid plugin (plugin.json file is missing) ({folder_name or temp_folder_name})",
|
||||
"error",
|
||||
)
|
||||
except JSONDecodeError as e:
|
||||
errors += 1
|
||||
error = 1
|
||||
flash(
|
||||
f"The file plugin.json in {file} is not valid ({e.msg}: line {e.lineno} column {e.colno} (char {e.pos})) ({folder_name or temp_folder_name})",
|
||||
"error",
|
||||
)
|
||||
except ValueError:
|
||||
errors += 1
|
||||
error = 1
|
||||
flash(
|
||||
f"The file plugin.json is missing one or more of the following keys: <i>{', '.join(PLUGIN_KEYS)}</i> ({folder_name or temp_folder_name})",
|
||||
"error",
|
||||
)
|
||||
except FileExistsError:
|
||||
errors += 1
|
||||
error = 1
|
||||
flash(
|
||||
f"A plugin named {folder_name} already exists",
|
||||
"error",
|
||||
)
|
||||
except (TarError, OSError) as e:
|
||||
errors += 1
|
||||
error = 1
|
||||
flash(str(e), "error")
|
||||
except Exception as e:
|
||||
errors += 1
|
||||
error = 1
|
||||
flash(str(e), "error")
|
||||
finally:
|
||||
if error != 1:
|
||||
flash(f"Successfully created plugin: <b><i>{folder_name}</i></b>")
|
||||
|
||||
error = 0
|
||||
|
||||
if errors >= files_count:
|
||||
return redirect(url_for("loading", next=url_for("plugins.plugins_page")))
|
||||
|
||||
db_metadata = DB.get_metadata()
|
||||
|
||||
def update_plugins(threaded: bool = False):
|
||||
wait_applying()
|
||||
|
||||
plugins = BW_CONFIG.get_plugins(_type="ui", with_data=True)
|
||||
for plugin in deepcopy(plugins):
|
||||
if plugin["id"] in new_plugins_ids:
|
||||
flash(f"Plugin {plugin['id']} already exists", "error")
|
||||
del new_plugins[new_plugins_ids.index(plugin["id"])]
|
||||
|
||||
err = DB.update_external_plugins(new_plugins, _type="ui", delete_missing=False)
|
||||
if err:
|
||||
message = f"Couldn't update ui plugins to database: {err}"
|
||||
if threaded:
|
||||
DATA["TO_FLASH"].append({"content": message, "type": "error"})
|
||||
else:
|
||||
flash(message, "error")
|
||||
else:
|
||||
update_plugins()
|
||||
message = "Plugins uploaded successfully"
|
||||
if threaded:
|
||||
DATA["TO_FLASH"].append({"content": message, "type": "success"})
|
||||
else:
|
||||
flash("Plugins uploaded successfully")
|
||||
|
||||
DATA["RELOADING"] = False
|
||||
|
||||
if any(
|
||||
v
|
||||
for k, v in db_metadata.items()
|
||||
if k in ("custom_configs_changed", "external_plugins_changed", "pro_plugins_changed", "plugins_config_changed", "instances_changed")
|
||||
):
|
||||
DATA["RELOADING"] = True
|
||||
DATA["LAST_RELOAD"] = time()
|
||||
|
||||
Thread(target=update_plugins, args=(True,)).start()
|
||||
else:
|
||||
update_plugins()
|
||||
|
||||
return redirect(url_for("loading", next=url_for("plugins.plugins_page"), message="Reloading plugins"))
|
||||
|
||||
|
|
@ -305,14 +345,68 @@ def plugins_page():
|
|||
if tmp_ui_path.is_dir():
|
||||
rmtree(tmp_ui_path, ignore_errors=True)
|
||||
|
||||
builder = plugins_builder(current_app.db.get_plugins())
|
||||
builder = plugins_builder(DB.get_plugins())
|
||||
return render_template("plugins.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
|
||||
|
||||
@plugins.route("/plugins/delete", methods=["POST"])
|
||||
@login_required
|
||||
def delete_plugin():
|
||||
if DB.readonly:
|
||||
return {"status": "ko", "message": "Database is in read-only mode"}, 403
|
||||
|
||||
verify_data_in_form(
|
||||
data={"plugin_name": None},
|
||||
err_message="Missing plugin name parameter on /plugins/delete.",
|
||||
redirect_url="plugins",
|
||||
next=True,
|
||||
)
|
||||
|
||||
plugin = request.form["plugin_name"]
|
||||
db_metadata = DB.get_metadata()
|
||||
|
||||
def update_plugins(threaded: bool = False): # type: ignore
|
||||
wait_applying()
|
||||
|
||||
err = DB.delete_plugin(plugins, "ui")
|
||||
if err:
|
||||
if not err.startswith("Plugin with id"):
|
||||
message = f"Couldn't delete plugin {plugin} in database: {err}"
|
||||
else:
|
||||
message = err
|
||||
|
||||
if threaded:
|
||||
DATA["TO_FLASH"].append({"content": message, "type": "error"})
|
||||
else:
|
||||
error_message(message)
|
||||
else:
|
||||
message = f"Deleted plugin {plugin} successfully"
|
||||
if threaded:
|
||||
DATA["TO_FLASH"].append({"content": message, "type": "success"})
|
||||
else:
|
||||
flash(message)
|
||||
|
||||
DATA["RELOADING"] = False
|
||||
|
||||
if any(
|
||||
v
|
||||
for k, v in db_metadata.items()
|
||||
if k in ("custom_configs_changed", "external_plugins_changed", "pro_plugins_changed", "plugins_config_changed", "instances_changed")
|
||||
):
|
||||
DATA["RELOADING"] = True
|
||||
DATA["LAST_RELOAD"] = time()
|
||||
|
||||
Thread(target=update_plugins, args=(True,)).start()
|
||||
else:
|
||||
update_plugins()
|
||||
|
||||
return redirect(url_for("loading", next=url_for("plugins.plugins_page"), message=f"Deleting plugin {plugin}"))
|
||||
|
||||
|
||||
@plugins.route("/plugins/upload", methods=["POST"])
|
||||
@login_required
|
||||
def upload_plugin():
|
||||
if current_app.db.readonly:
|
||||
if DB.readonly:
|
||||
return {"status": "ko", "message": "Database is in read-only mode"}, 403
|
||||
|
||||
if not request.files:
|
||||
|
|
@ -392,7 +486,7 @@ def custom_plugin(plugin: str):
|
|||
if request.method == "GET":
|
||||
|
||||
# Check plugin's page
|
||||
page = current_app.db.get_plugin_page(plugin)
|
||||
page = DB.get_plugin_page(plugin)
|
||||
|
||||
if not page:
|
||||
return error_message("The plugin does not have a page"), 404
|
||||
|
|
@ -405,10 +499,10 @@ def custom_plugin(plugin: str):
|
|||
|
||||
tmp_page_dir = tmp_page_dir.joinpath("ui")
|
||||
|
||||
current_app.logger.debug(f"Plugin {plugin} page extracted successfully")
|
||||
LOGGER.debug(f"Plugin {plugin} page extracted successfully")
|
||||
|
||||
# Case template, prepare data
|
||||
plugins = current_app.bw_config.get_plugins()
|
||||
plugins = BW_CONFIG.get_plugins()
|
||||
plugin_id = None
|
||||
curr_plugin = {}
|
||||
is_used = False
|
||||
|
|
@ -426,7 +520,7 @@ def custom_plugin(plugin: str):
|
|||
if plugin_id is None:
|
||||
return error_message("Plugin not found"), 404
|
||||
|
||||
config = current_app.db.get_config()
|
||||
config = DB.get_config()
|
||||
|
||||
# Check if we are using metrics
|
||||
for service in config.get("SERVER_NAME", "").split(" "):
|
||||
|
|
@ -514,14 +608,14 @@ def custom_plugin(plugin: str):
|
|||
action_result = run_action(plugin)
|
||||
|
||||
if isinstance(action_result, Response):
|
||||
current_app.logger.info(f"Plugin {plugin} action executed successfully")
|
||||
LOGGER.info(f"Plugin {plugin} action executed successfully")
|
||||
return action_result
|
||||
|
||||
# case error
|
||||
if action_result["status"] == "ko":
|
||||
return error_message(action_result["message"]), action_result["code"]
|
||||
|
||||
current_app.logger.info(f"Plugin {plugin} action executed successfully")
|
||||
LOGGER.info(f"Plugin {plugin} action executed successfully")
|
||||
|
||||
if request.content_type == "application/x-www-form-urlencoded":
|
||||
return redirect(f"{url_for('plugins.plugins_page')}/{plugin}", code=303)
|
||||
|
|
|
|||
|
|
@ -2,16 +2,18 @@ from base64 import b64encode
|
|||
from json import dumps
|
||||
from threading import Thread
|
||||
from time import time
|
||||
from flask import Blueprint, current_app, flash, redirect, render_template, request, url_for, session
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for, session
|
||||
from flask_login import current_user, login_required, logout_user
|
||||
|
||||
from builder.profile import profile_builder # type: ignore
|
||||
|
||||
from utils import USER_PASSWORD_RX, gen_password_hash
|
||||
from src.totp import totp as TOTP
|
||||
|
||||
from dependencies import BW_CONFIG, DATA, DB
|
||||
from utils import LOGGER, USER_PASSWORD_RX, gen_password_hash
|
||||
|
||||
from pages.utils import handle_error, manage_bunkerweb, verify_data_in_form, wait_applying
|
||||
|
||||
|
||||
profile = Blueprint("profile", __name__)
|
||||
|
||||
|
||||
|
|
@ -20,7 +22,7 @@ profile = Blueprint("profile", __name__)
|
|||
def profile_page():
|
||||
totp_recovery_codes = None
|
||||
if request.method == "POST":
|
||||
if current_app.db.readonly:
|
||||
if DB.readonly:
|
||||
return handle_error("Database is in read-only mode", "profile")
|
||||
|
||||
verify_data_in_form(
|
||||
|
|
@ -38,18 +40,18 @@ def profile_page():
|
|||
|
||||
variables = {"PRO_LICENSE_KEY": request.form["license"]}
|
||||
|
||||
variables = current_app.bw_config.check_variables(variables, {"PRO_LICENSE_KEY": request.form["license"]})
|
||||
variables = BW_CONFIG.check_variables(variables, {"PRO_LICENSE_KEY": request.form["license"]})
|
||||
|
||||
if not variables:
|
||||
return handle_error("The license key variable checks returned error", "profile", True)
|
||||
|
||||
# Force job to contact PRO API
|
||||
# by setting the last check to None
|
||||
metadata = current_app.db.get_metadata()
|
||||
metadata = DB.get_metadata()
|
||||
metadata["last_pro_check"] = None
|
||||
current_app.db.set_pro_metadata(metadata)
|
||||
DB.set_pro_metadata(metadata)
|
||||
|
||||
curr_changes = current_app.db.check_changes()
|
||||
curr_changes = DB.check_changes()
|
||||
|
||||
# Reload instances
|
||||
def update_global_config(threaded: bool = False):
|
||||
|
|
@ -58,16 +60,16 @@ def profile_page():
|
|||
if not manage_bunkerweb("global_config", variables, threaded=threaded):
|
||||
message = "Checking license key to upgrade."
|
||||
if threaded:
|
||||
current_app.data["TO_FLASH"].append({"content": message, "type": "success"})
|
||||
DATA["TO_FLASH"].append({"content": message, "type": "success"})
|
||||
else:
|
||||
flash(message)
|
||||
|
||||
current_app.data["PRO_LOADING"] = True
|
||||
current_app.data["CONFIG_CHANGED"] = True
|
||||
DATA["PRO_LOADING"] = True
|
||||
DATA["CONFIG_CHANGED"] = True
|
||||
|
||||
if any(curr_changes.values()):
|
||||
current_app.data["RELOADING"] = True
|
||||
current_app.data["LAST_RELOAD"] = time()
|
||||
DATA["RELOADING"] = True
|
||||
DATA["LAST_RELOAD"] = time()
|
||||
Thread(target=update_global_config, args=(True,)).start()
|
||||
else:
|
||||
update_global_config()
|
||||
|
|
@ -119,25 +121,25 @@ def profile_page():
|
|||
if request.form["operation"] == "totp":
|
||||
verify_data_in_form(data={"totp_token": None}, err_message="Missing totp token parameter on /account.", redirect_url="profile")
|
||||
|
||||
if not current_app.totp.verify_totp(
|
||||
if not TOTP.verify_totp(
|
||||
request.form["totp_token"], totp_secret=session.get("tmp_totp_secret", ""), user=current_user
|
||||
) and not current_app.totp.verify_recovery_code(request.form["totp_token"], user=current_user):
|
||||
) and not TOTP.verify_recovery_code(request.form["totp_token"], user=current_user):
|
||||
return handle_error("The totp token is invalid. (totp)", "profile")
|
||||
|
||||
session["totp_validated"] = not bool(current_user.totp_secret)
|
||||
totp_secret = None if bool(current_user.totp_secret) else session.pop("tmp_totp_secret", "")
|
||||
|
||||
if totp_secret and totp_secret != current_user.totp_secret:
|
||||
totp_recovery_codes = current_app.totp.generate_recovery_codes()
|
||||
totp_recovery_codes = TOTP.generate_recovery_codes()
|
||||
flash(
|
||||
"The recovery codes have been refreshed.\nPlease save them in a safe place. They will not be displayed again."
|
||||
+ "\n".join(totp_recovery_codes),
|
||||
"info",
|
||||
) # TODO: Remove this when we have a way to display the recovery codes
|
||||
|
||||
current_app.logger.debug(f"totp recovery codes: {totp_recovery_codes or current_user.list_recovery_codes}")
|
||||
LOGGER.debug(f"totp recovery codes: {totp_recovery_codes or current_user.list_recovery_codes}")
|
||||
|
||||
ret = current_app.db.update_ui_user(
|
||||
ret = DB.update_ui_user(
|
||||
username,
|
||||
gen_password_hash(password),
|
||||
totp_secret,
|
||||
|
|
@ -159,8 +161,8 @@ def profile_page():
|
|||
|
||||
totp_qr_image = ""
|
||||
if not bool(current_user.totp_secret):
|
||||
session["tmp_totp_secret"] = current_app.totp.generate_totp_secret()
|
||||
totp_qr_image = current_app.totp.generate_qrcode(current_user.get_id(), session["tmp_totp_secret"])
|
||||
session["tmp_totp_secret"] = TOTP.generate_totp_secret()
|
||||
totp_qr_image = TOTP.generate_qrcode(current_user.get_id(), session["tmp_totp_secret"])
|
||||
|
||||
builder = profile_builder(
|
||||
current_user if current_user.is_authenticated else None,
|
||||
|
|
@ -169,7 +171,7 @@ def profile_page():
|
|||
"totp_image": totp_qr_image,
|
||||
"totp_recovery_codes": totp_recovery_codes or current_user.list_recovery_codes,
|
||||
"is_recovery_refreshed": bool(totp_recovery_codes),
|
||||
"totp_secret": current_app.totp.get_totp_pretty_key(session.get("tmp_totp_secret", "")),
|
||||
"totp_secret": TOTP.get_totp_pretty_key(session.get("tmp_totp_secret", "")),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ from base64 import b64encode
|
|||
from json import dumps
|
||||
from math import floor
|
||||
|
||||
from flask import Blueprint, current_app, render_template
|
||||
from flask import Blueprint, render_template
|
||||
from flask_login import login_required
|
||||
|
||||
from dependencies import BW_INSTANCES_UTILS
|
||||
|
||||
from builder.reports import reports_builder # type: ignore
|
||||
|
||||
reports = Blueprint("reports", __name__)
|
||||
|
|
@ -13,7 +15,7 @@ reports = Blueprint("reports", __name__)
|
|||
@reports.route("/reports", methods=["GET"])
|
||||
@login_required
|
||||
def reports_page():
|
||||
reports = current_app.bw_instances_utils.get_reports()
|
||||
reports = BW_INSTANCES_UTILS.get_reports()
|
||||
reasons = set()
|
||||
countries = set()
|
||||
methods = set()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
from base64 import b64encode
|
||||
from json import dumps
|
||||
|
||||
from flask import Blueprint, current_app, redirect, render_template, request, url_for
|
||||
from flask import Blueprint, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from builder.services import services_builder # type: ignore
|
||||
|
||||
from pages.utils import get_service_data, handle_error, update_service
|
||||
from dependencies import DB
|
||||
|
||||
from pages.utils import get_service_data, handle_error, update_service
|
||||
|
||||
services = Blueprint("services", __name__)
|
||||
|
||||
|
|
@ -16,7 +17,7 @@ services = Blueprint("services", __name__)
|
|||
@login_required
|
||||
def services_page():
|
||||
if request.method == "POST":
|
||||
if current_app.db.readonly:
|
||||
if DB.readonly:
|
||||
return handle_error("Database is in read-only mode", "services")
|
||||
|
||||
config, variables, format_configs, server_name, old_server_name, operation, is_draft, was_draft, is_draft_unchanged, mode = get_service_data("services")
|
||||
|
|
@ -27,7 +28,7 @@ def services_page():
|
|||
|
||||
# Display services
|
||||
services = []
|
||||
tmp_config = current_app.db.get_config(methods=True, with_drafts=True).copy()
|
||||
tmp_config = DB.get_config(methods=True, with_drafts=True).copy()
|
||||
service_names = tmp_config["SERVER_NAME"]["value"].split(" ")
|
||||
|
||||
table_settings = (
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ from string import ascii_letters, digits
|
|||
from threading import Thread
|
||||
from time import time
|
||||
|
||||
from flask import Blueprint, Response, current_app, flash, redirect, render_template, request, url_for
|
||||
from flask import Blueprint, Response, flash, redirect, render_template, request, url_for
|
||||
|
||||
from dependencies import BW_CONFIG, DATA, DB
|
||||
from utils import USER_PASSWORD_RX, gen_password_hash
|
||||
|
||||
from pages.utils import REVERSE_PROXY_PATH, handle_error, manage_bunkerweb
|
||||
|
|
@ -15,9 +16,9 @@ setup = Blueprint("setup", __name__)
|
|||
|
||||
@setup.route("/setup", methods=["GET", "POST"])
|
||||
def setup_page():
|
||||
db_config = current_app.bw_config.get_config(methods=False, filtered_settings=("SERVER_NAME", "MULTISITE", "USE_UI", "UI_HOST", "AUTO_LETS_ENCRYPT"))
|
||||
db_config = BW_CONFIG.get_config(methods=False, filtered_settings=("SERVER_NAME", "MULTISITE", "USE_UI", "UI_HOST", "AUTO_LETS_ENCRYPT"))
|
||||
|
||||
admin_user = current_app.db.get_ui_user()
|
||||
admin_user = DB.get_ui_user()
|
||||
|
||||
ui_reverse_proxy = False
|
||||
for server_name in db_config["SERVER_NAME"].split(" "):
|
||||
|
|
@ -28,7 +29,7 @@ def setup_page():
|
|||
break
|
||||
|
||||
if request.method == "POST":
|
||||
if current_app.db.readonly:
|
||||
if DB.readonly:
|
||||
return handle_error("Database is in read-only mode", "setup")
|
||||
|
||||
required_keys = []
|
||||
|
|
@ -53,9 +54,7 @@ def setup_page():
|
|||
"setup",
|
||||
)
|
||||
|
||||
ret = current_app.db.create_ui_user(
|
||||
request.form["admin_username"], gen_password_hash(request.form["admin_password"]), ["admin"], method="ui", admin=True
|
||||
)
|
||||
ret = DB.create_ui_user(request.form["admin_username"], gen_password_hash(request.form["admin_password"]), ["admin"], method="ui", admin=True)
|
||||
if ret:
|
||||
return handle_error(f"Couldn't create the admin user in the database: {ret}", "setup", False, "error")
|
||||
|
||||
|
|
@ -73,8 +72,8 @@ def setup_page():
|
|||
if not REVERSE_PROXY_PATH.match(request.form["ui_host"]):
|
||||
return handle_error("The hostname is not valid.", "setup")
|
||||
|
||||
current_app.data["RELOADING"] = True
|
||||
current_app.data["LAST_RELOAD"] = time()
|
||||
DATA["RELOADING"] = True
|
||||
DATA["LAST_RELOAD"] = time()
|
||||
|
||||
config = {
|
||||
"SERVER_NAME": request.form["server_name"],
|
||||
|
|
@ -95,7 +94,7 @@ def setup_page():
|
|||
config["SELF_SIGNED_SSL_SUBJ"] = f"/CN={request.form['server_name']}/"
|
||||
|
||||
if not config.get("MULTISITE", "no") == "yes":
|
||||
current_app.bw_config.edit_global_conf({"MULTISITE": "yes"}, check_changes=False)
|
||||
BW_CONFIG.edit_global_conf({"MULTISITE": "yes"}, check_changes=False)
|
||||
|
||||
# deepcode ignore MissingAPI: We don't need to check to wait for the thread to finish
|
||||
Thread(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
from flask import Blueprint, current_app, redirect, render_template, request, session, url_for
|
||||
from flask import Blueprint, redirect, render_template, request, session, url_for
|
||||
from flask_login import current_user, login_required
|
||||
|
||||
from pages.utils import handle_error, verify_data_in_form
|
||||
from src.totp import totp as TOTP
|
||||
|
||||
from dependencies import DB
|
||||
|
||||
from pages.utils import handle_error, verify_data_in_form
|
||||
|
||||
totp = Blueprint("totp", __name__)
|
||||
|
||||
|
|
@ -13,11 +16,11 @@ def totp_page():
|
|||
if request.method == "POST":
|
||||
verify_data_in_form(data={"totp_token": None}, err_message="No token provided on /totp.", redirect_url="totp")
|
||||
|
||||
if not current_app.totp.verify_totp(request.form["totp_token"], user=current_user):
|
||||
recovery_code = current_app.totp.verify_recovery_code(request.form["totp_token"], user=current_user)
|
||||
if not TOTP.verify_totp(request.form["totp_token"], user=current_user):
|
||||
recovery_code = TOTP.verify_recovery_code(request.form["totp_token"], user=current_user)
|
||||
if not recovery_code:
|
||||
return handle_error("The token is invalid.", "totp")
|
||||
current_app.db.use_ui_user_recovery_code(current_user.get_id(), recovery_code)
|
||||
DB.use_ui_user_recovery_code(current_user.get_id(), recovery_code)
|
||||
|
||||
session["totp_validated"] = True
|
||||
redirect(url_for("loading", next=request.form.get("next") or url_for("home.home_page"), message="Validating TOTP token."))
|
||||
|
|
|
|||
|
|
@ -1,25 +1,20 @@
|
|||
from base64 import b64encode
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
from importlib.machinery import SourceFileLoader
|
||||
from io import BytesIO
|
||||
from os.path import sep
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
from sys import path as sys_path
|
||||
from tarfile import open as tar_open
|
||||
from threading import Thread
|
||||
from time import sleep, time
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
from uuid import uuid4
|
||||
|
||||
from flask import Response, current_app, flash, redirect, request, url_for
|
||||
from flask import Response, flash, redirect, request, url_for
|
||||
from qrcode.main import QRCode
|
||||
from regex import compile as re_compile
|
||||
|
||||
from src.instance import Instance
|
||||
|
||||
TMP_DIR = Path(sep, "var", "tmp", "bunkerweb")
|
||||
from dependencies import BW_CONFIG, DATA, DB
|
||||
from utils import LOGGER
|
||||
|
||||
|
||||
LOG_RX = re_compile(r"^(?P<date>\d+/\d+/\d+\s\d+:\d+:\d+)\s\[(?P<level>[a-z]+)\]\s\d+#\d+:\s(?P<message>[^\n]+)$")
|
||||
REVERSE_PROXY_PATH = re_compile(r"^(?P<host>https?://.{1,255}(:((6553[0-5])|(655[0-2]\d)|(65[0-4]\d{2})|(6[0-4]\d{3})|([1-5]\d{4})|([0-5]{0,5})|(\d{1,4})))?)$")
|
||||
|
|
@ -31,9 +26,9 @@ def wait_applying():
|
|||
current_time = datetime.now(timezone.utc)
|
||||
ready = False
|
||||
while not ready and (datetime.now(timezone.utc) - current_time).seconds < 120:
|
||||
db_metadata = current_app.db.get_metadata()
|
||||
db_metadata = DB.get_metadata()
|
||||
if isinstance(db_metadata, str):
|
||||
current_app.logger.error(f"An error occurred when checking for changes in the database : {db_metadata}")
|
||||
LOGGER.error(f"An error occurred when checking for changes in the database : {db_metadata}")
|
||||
elif not any(
|
||||
v
|
||||
for k, v in db_metadata.items()
|
||||
|
|
@ -42,11 +37,11 @@ def wait_applying():
|
|||
ready = True
|
||||
continue
|
||||
else:
|
||||
current_app.logger.warning("Scheduler is already applying a configuration, retrying in 1s ...")
|
||||
LOGGER.warning("Scheduler is already applying a configuration, retrying in 1s ...")
|
||||
sleep(1)
|
||||
|
||||
if not ready:
|
||||
current_app.logger.error("Too many retries while waiting for scheduler to apply configuration...")
|
||||
LOGGER.error("Too many retries while waiting for scheduler to apply configuration...")
|
||||
|
||||
|
||||
# TODO: Find a more elegant way to handle this
|
||||
|
|
@ -54,45 +49,45 @@ def manage_bunkerweb(method: str, *args, operation: str = "reloads", is_draft: b
|
|||
# Do the operation
|
||||
error = 0
|
||||
|
||||
if "TO_FLASH" not in current_app.data:
|
||||
current_app.data["TO_FLASH"] = []
|
||||
if "TO_FLASH" not in DATA:
|
||||
DATA["TO_FLASH"] = []
|
||||
|
||||
if method == "services":
|
||||
if operation == "new":
|
||||
operation, error = current_app.bw_config.new_service(args[0], is_draft=is_draft)
|
||||
operation, error = BW_CONFIG.new_service(args[0], is_draft=is_draft)
|
||||
elif operation == "edit":
|
||||
operation, error = current_app.bw_config.edit_service(args[1], args[0], check_changes=(was_draft != is_draft or not is_draft), is_draft=is_draft)
|
||||
operation, error = BW_CONFIG.edit_service(args[1], args[0], check_changes=(was_draft != is_draft or not is_draft), is_draft=is_draft)
|
||||
elif operation == "delete":
|
||||
operation, error = current_app.bw_config.delete_service(args[2], check_changes=(was_draft != is_draft or not is_draft))
|
||||
operation, error = BW_CONFIG.delete_service(args[2], check_changes=(was_draft != is_draft or not is_draft))
|
||||
elif method == "global_config":
|
||||
operation, error = current_app.bw_config.edit_global_conf(args[0], check_changes=True)
|
||||
operation, error = BW_CONFIG.edit_global_conf(args[0], check_changes=True)
|
||||
|
||||
if operation == "reload":
|
||||
instance = Instance.from_hostname(args[0], current_app.db)
|
||||
instance = Instance.from_hostname(args[0], DB)
|
||||
if instance:
|
||||
operation = instance.reload()
|
||||
else:
|
||||
operation = "The instance does not exist."
|
||||
elif operation == "start":
|
||||
instance = Instance.from_hostname(args[0], current_app.db)
|
||||
instance = Instance.from_hostname(args[0], DB)
|
||||
if instance:
|
||||
operation = instance.start()
|
||||
else:
|
||||
operation = "The instance does not exist."
|
||||
elif operation == "stop":
|
||||
instance = Instance.from_hostname(args[0], current_app.db)
|
||||
instance = Instance.from_hostname(args[0], DB)
|
||||
if instance:
|
||||
operation = instance.stop()
|
||||
else:
|
||||
operation = "The instance does not exist."
|
||||
elif operation == "restart":
|
||||
instance = Instance.from_hostname(args[0], current_app.db)
|
||||
instance = Instance.from_hostname(args[0], DB)
|
||||
if instance:
|
||||
operation = instance.restart()
|
||||
else:
|
||||
operation = "The instance does not exist."
|
||||
elif operation == "ping":
|
||||
instance = Instance.from_hostname(args[0], current_app.db)
|
||||
instance = Instance.from_hostname(args[0], DB)
|
||||
if instance:
|
||||
operation = instance.ping()[0]
|
||||
else:
|
||||
|
|
@ -103,125 +98,46 @@ def manage_bunkerweb(method: str, *args, operation: str = "reloads", is_draft: b
|
|||
if operation:
|
||||
if isinstance(operation, list):
|
||||
for op in operation:
|
||||
current_app.data["TO_FLASH"].append({"content": f"Reload failed for the instance {op}", "type": "error"})
|
||||
DATA["TO_FLASH"].append({"content": f"Reload failed for the instance {op}", "type": "error"})
|
||||
elif operation.startswith(("Can't", "The database is read-only")):
|
||||
current_app.data["TO_FLASH"].append({"content": operation, "type": "error"})
|
||||
DATA["TO_FLASH"].append({"content": operation, "type": "error"})
|
||||
else:
|
||||
current_app.data["TO_FLASH"].append({"content": operation, "type": "success"})
|
||||
DATA["TO_FLASH"].append({"content": operation, "type": "success"})
|
||||
|
||||
if not threaded:
|
||||
for f in current_app.data.get("TO_FLASH", []):
|
||||
for f in DATA.get("TO_FLASH", []):
|
||||
if f["type"] == "error":
|
||||
flash(f["content"], "error")
|
||||
else:
|
||||
flash(f["content"])
|
||||
|
||||
current_app.data["TO_FLASH"] = []
|
||||
DATA["TO_FLASH"] = []
|
||||
|
||||
current_app.data["RELOADING"] = False
|
||||
DATA["RELOADING"] = False
|
||||
|
||||
return error
|
||||
|
||||
|
||||
def run_action(plugin: str, function_name: str = "", *, tmp_dir: Optional[Path] = None) -> Union[dict, Response]:
|
||||
message = ""
|
||||
if not tmp_dir:
|
||||
page = current_app.db.get_plugin_page(plugin)
|
||||
|
||||
if not page:
|
||||
return {"status": "ko", "code": 404, "message": "The plugin does not have a page"}
|
||||
|
||||
try:
|
||||
# Try to import the plugin's custom page
|
||||
tmp_dir = TMP_DIR.joinpath("ui", "action", str(uuid4()))
|
||||
tmp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with tar_open(fileobj=BytesIO(page), mode="r:gz") as tar:
|
||||
tar.extractall(tmp_dir)
|
||||
|
||||
tmp_dir = tmp_dir.joinpath("ui")
|
||||
except BaseException as e:
|
||||
current_app.logger.error(f"An error occurred while extracting the plugin: {e}")
|
||||
return {"status": "ko", "code": 500, "message": "An error occurred while extracting the plugin, see logs for more details"}
|
||||
|
||||
try:
|
||||
action_file = tmp_dir.joinpath("actions.py")
|
||||
if not action_file.is_file():
|
||||
return {"status": "ko", "code": 404, "message": "The plugin does not have an action file"}
|
||||
|
||||
sys_path.append(tmp_dir.as_posix())
|
||||
loader = SourceFileLoader("actions", action_file.as_posix())
|
||||
actions = loader.load_module()
|
||||
except BaseException as e:
|
||||
sys_path.pop()
|
||||
if function_name != "pre_render":
|
||||
rmtree(tmp_dir, ignore_errors=True)
|
||||
|
||||
current_app.logger.error(f"An error occurred while importing the plugin: {e}")
|
||||
return {"status": "ko", "code": 500, "message": "An error occurred while importing the plugin, see logs for more details"}
|
||||
|
||||
exception = None
|
||||
res = None
|
||||
message = None
|
||||
|
||||
try:
|
||||
# Try to get the custom plugin custom function and call it
|
||||
method = getattr(actions, function_name or plugin)
|
||||
queries = request.args.to_dict()
|
||||
try:
|
||||
data = request.json or {}
|
||||
except BaseException:
|
||||
data = {}
|
||||
|
||||
res = method(current_app=current_app, args=queries, data=data)
|
||||
except AttributeError as e:
|
||||
if function_name == "pre_render":
|
||||
sys_path.pop()
|
||||
return {"status": "ok", "code": 200, "message": "The plugin does not have a pre_render method"}
|
||||
|
||||
message = "The plugin does not have a method"
|
||||
exception = e
|
||||
except BaseException as e:
|
||||
message = "An error occurred while executing the plugin"
|
||||
exception = e
|
||||
finally:
|
||||
sys_path.pop()
|
||||
|
||||
if function_name != "pre_render":
|
||||
rmtree(tmp_dir, ignore_errors=True)
|
||||
|
||||
if message:
|
||||
current_app.logger.error(message + (f": {exception}" if exception else ""))
|
||||
if message or not isinstance(res, dict) and not res:
|
||||
return {
|
||||
"status": "ko",
|
||||
"code": 500,
|
||||
"message": message + ", see logs for more details" if message else "The plugin did not return a valid response",
|
||||
}
|
||||
|
||||
if isinstance(res, Response):
|
||||
return res
|
||||
|
||||
return {"status": "ok", "code": 200, "data": res}
|
||||
|
||||
|
||||
def verify_data_in_form(
|
||||
data: Optional[Dict[str, Union[Tuple, Any]]] = None, err_message: str = "", redirect_url: str = "", next: bool = False
|
||||
) -> Union[bool, Response]:
|
||||
current_app.logger.debug(f"Verifying data in form: {data}")
|
||||
current_app.logger.debug(f"Request form: {request.form}")
|
||||
if not request.json:
|
||||
return handle_error("Invalid request", redirect_url, next, "error")
|
||||
|
||||
LOGGER.debug(f"Verifying data in form: {data}")
|
||||
LOGGER.debug(f"Request form: {request.form}")
|
||||
|
||||
# Loop on each key in data
|
||||
for key, values in (data or {}).items():
|
||||
if key not in request.form:
|
||||
return handle_error(f"Missing {key} in form", f"{redirect_url}.{redirect_url}_page", next, "error")
|
||||
return handle_error(f"Missing {key} in form", redirect_url, next, "error")
|
||||
|
||||
# Case we want to only check if key is in form, we can skip the values check by setting values to falsy value
|
||||
if not values:
|
||||
continue
|
||||
|
||||
if request.form[key] not in values:
|
||||
return handle_error(err_message, f"{redirect_url}.{redirect_url}_page", next, "error")
|
||||
return handle_error(err_message, redirect_url, next, "error")
|
||||
|
||||
return True
|
||||
|
||||
|
|
@ -231,10 +147,10 @@ def handle_error(err_message: str = "", redirect_url: str = "", next: bool = Fal
|
|||
flash(err_message, "error")
|
||||
|
||||
if log == "error":
|
||||
current_app.logger.error(err_message)
|
||||
LOGGER.error(err_message)
|
||||
|
||||
if log == "exception":
|
||||
current_app.logger.exception(err_message)
|
||||
LOGGER.exception(err_message)
|
||||
|
||||
if not redirect_url:
|
||||
return False
|
||||
|
|
@ -246,7 +162,7 @@ def handle_error(err_message: str = "", redirect_url: str = "", next: bool = Fal
|
|||
|
||||
|
||||
def error_message(msg: str):
|
||||
current_app.logger.error(msg)
|
||||
LOGGER.error(msg)
|
||||
return {"status": "ko", "message": msg}
|
||||
|
||||
|
||||
|
|
@ -315,7 +231,7 @@ def get_service_data(page_name: str):
|
|||
redirect_url="services",
|
||||
)
|
||||
|
||||
config = current_app.db.get_config(methods=True, with_drafts=True)
|
||||
config = DB.get_config(methods=True, with_drafts=True)
|
||||
# Check variables
|
||||
variables = deepcopy(request.form.to_dict())
|
||||
mode = variables.pop("mode", None)
|
||||
|
|
@ -387,7 +303,7 @@ def get_service_data(page_name: str):
|
|||
):
|
||||
del variables[variable]
|
||||
|
||||
variables = current_app.bw_config.check_variables(variables, config)
|
||||
variables = BW_CONFIG.check_variables(variables, config)
|
||||
return config, variables, format_configs, server_name, old_server_name, operation, is_draft, was_draft, is_draft_unchanged, mode
|
||||
|
||||
|
||||
|
|
@ -402,7 +318,7 @@ def update_service(config, variables, format_configs, server_name, old_server_na
|
|||
# Delete
|
||||
if request.form["operation"] == "delete":
|
||||
|
||||
is_service = current_app.bw_config.check_variables({"SERVER_NAME": request.form["SERVER_NAME"]}, config)
|
||||
is_service = BW_CONFIG.check_variables({"SERVER_NAME": request.form["SERVER_NAME"]}, config)
|
||||
|
||||
if not is_service:
|
||||
error_message(f"Error while deleting the service {request.form['SERVER_NAME']}")
|
||||
|
|
@ -410,7 +326,7 @@ def update_service(config, variables, format_configs, server_name, old_server_na
|
|||
if config.get(f"{request.form['SERVER_NAME'].split(' ')[0]}_SERVER_NAME", {"method": "scheduler"})["method"] != "ui":
|
||||
return handle_error("The service cannot be deleted because it has not been created with the UI.", "services", True)
|
||||
|
||||
db_metadata = current_app.db.get_metadata()
|
||||
db_metadata = DB.get_metadata()
|
||||
|
||||
def update_services(threaded: bool = False):
|
||||
wait_applying()
|
||||
|
|
@ -431,13 +347,13 @@ def update_service(config, variables, format_configs, server_name, old_server_na
|
|||
for k, v in db_metadata.items()
|
||||
if k in ("custom_configs_changed", "external_plugins_changed", "pro_plugins_changed", "plugins_config_changed", "instances_changed")
|
||||
):
|
||||
current_app.data["RELOADING"] = True
|
||||
current_app.data["LAST_RELOAD"] = time()
|
||||
DATA["RELOADING"] = True
|
||||
DATA["LAST_RELOAD"] = time()
|
||||
Thread(target=update_services, args=(True,)).start()
|
||||
else:
|
||||
update_services()
|
||||
|
||||
current_app.data["CONFIG_CHANGED"] = True
|
||||
DATA["CONFIG_CHANGED"] = True
|
||||
|
||||
message = ""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from os import sep
|
||||
from os.path import join
|
||||
from pathlib import Path
|
||||
from re import compile as re_compile
|
||||
|
||||
from utils import path_to_dict
|
||||
|
||||
|
||||
class CustomConfig:
|
||||
def __init__(self):
|
||||
self.__name_regex = re_compile(r"^[\w.-]{4,64}$")
|
||||
self.__root_dirs = [child["name"] for child in path_to_dict(join(sep, "etc", "bunkerweb", "configs"))["children"]]
|
||||
self.__file_creation_blacklist = ["http", "stream"]
|
||||
|
||||
def check_name(self, name: str) -> bool:
|
||||
return self.__name_regex.match(name) is not None
|
||||
|
||||
def check_path(self, path: str, root_path: str = join(sep, "etc", "bunkerweb", "configs")) -> str:
|
||||
root_dir: str = path.split("/")[4]
|
||||
if not (
|
||||
path.startswith(root_path)
|
||||
or root_path == join(sep, "etc", "bunkerweb", "configs")
|
||||
and path.startswith(root_path)
|
||||
and root_dir in self.__root_dirs
|
||||
and (not path.endswith(".conf") or root_dir not in self.__file_creation_blacklist or len(path.split("/")) > 5)
|
||||
):
|
||||
return f"{path} is not a valid path"
|
||||
|
||||
if root_path == join(sep, "etc", "bunkerweb", "configs"):
|
||||
dirs = path.split("/")[5:]
|
||||
nbr_children = len(dirs)
|
||||
dirs = "/".join(dirs)
|
||||
if len(dirs) > 1:
|
||||
for x in range(nbr_children - 1):
|
||||
if not Path(root_path, root_dir, "/".join(dirs.split("/")[0:-x])).exists():
|
||||
return f"{join(root_path, root_dir, '/'.join(dirs.split('/')[0:-x]))} doesn't exist"
|
||||
|
||||
return ""
|
||||
|
|
@ -1,25 +1,47 @@
|
|||
from base64 import b64encode
|
||||
from io import BytesIO
|
||||
from json import JSONDecodeError, loads as json_loads
|
||||
from os import getenv
|
||||
from bcrypt import checkpw
|
||||
from typing import Dict, List, Optional, Union
|
||||
from flask import Flask
|
||||
from typing import List, Optional
|
||||
from passlib.totp import TOTP, MalformedTokenError, TokenError, TotpMatch
|
||||
from passlib.pwd import genword
|
||||
from qrcode import make
|
||||
from qrcode.image.svg import SvgImage
|
||||
|
||||
from models import Users
|
||||
from dependencies import DATA
|
||||
from utils import LIB_DIR, LOGGER, stop
|
||||
|
||||
|
||||
TOTP_SECRETS = getenv("TOTP_SECRETS", "")
|
||||
if TOTP_SECRETS:
|
||||
try:
|
||||
TOTP_SECRETS = json_loads(TOTP_SECRETS)
|
||||
except JSONDecodeError:
|
||||
x = 1
|
||||
tmp_secrets = {}
|
||||
for secret in TOTP_SECRETS.strip().split(" "):
|
||||
if secret:
|
||||
tmp_secrets[x] = secret
|
||||
x += 1
|
||||
TOTP_SECRETS = tmp_secrets.copy()
|
||||
del tmp_secrets
|
||||
|
||||
if not TOTP_SECRETS:
|
||||
if not LIB_DIR.joinpath(".totp_secrets.json").is_file():
|
||||
LOGGER.error("The TOTP_SECRETS environment variable is missing and the .totp_secrets.json file is missing, exiting ...")
|
||||
stop(1)
|
||||
TOTP_SECRETS = json_loads(LIB_DIR.joinpath(".totp_secrets.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
class Totp:
|
||||
def __init__(self, app: Flask, secrets: Dict[Union[str, int], str]):
|
||||
def __init__(self):
|
||||
"""Initialize a totp factory.
|
||||
secrets are used to encrypt the per-user totp_secret on disk.
|
||||
recovery_codes_keys are used to encrypt the per-user recovery codes on disk.
|
||||
"""
|
||||
# This should be a dict with at least one entry
|
||||
self.app = app
|
||||
self._totp = TOTP.using(secrets=secrets, issuer="BunkerWeb UI")
|
||||
self._totp = TOTP.using(secrets=TOTP_SECRETS, issuer="BunkerWeb UI")
|
||||
|
||||
def generate_totp_secret(self) -> str:
|
||||
"""Create new user-unique totp_secret."""
|
||||
|
|
@ -73,10 +95,15 @@ class Totp:
|
|||
|
||||
def get_last_counter(self, user: Users) -> Optional[int]:
|
||||
"""Fetch stored last_counter from cache."""
|
||||
return self.app.data.get("totp_last_counter", {}).get(user.get_id())
|
||||
return DATA.get("totp_last_counter", {}).get(user.get_id())
|
||||
|
||||
def set_last_counter(self, user: Users, tmatch: TotpMatch) -> None:
|
||||
"""Cache last_counter."""
|
||||
if "totp_last_counter" not in self.app.data:
|
||||
self.app.data["totp_last_counter"] = {}
|
||||
self.app.data["totp_last_counter"][user.get_id()] = tmatch.counter
|
||||
if "totp_last_counter" not in DATA:
|
||||
DATA["totp_last_counter"] = {}
|
||||
DATA["totp_last_counter"][user.get_id()] = tmatch.counter
|
||||
|
||||
|
||||
totp = Totp()
|
||||
|
||||
__all__ = ("totp",)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,45 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from os.path import join
|
||||
from os import _exit, getenv
|
||||
from os.path import join, sep
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, Popen, call
|
||||
from typing import List, Optional
|
||||
|
||||
from bcrypt import checkpw, gensalt, hashpw
|
||||
from magic import Magic
|
||||
from regex import compile as re_compile
|
||||
|
||||
from logger import setup_logger # type: ignore
|
||||
|
||||
TMP_DIR = Path(sep, "var", "tmp", "bunkerweb")
|
||||
LIB_DIR = Path(sep, "var", "lib", "bunkerweb")
|
||||
|
||||
LOGGER = setup_logger("UI", getenv("CUSTOM_LOG_LEVEL", getenv("LOG_LEVEL", "INFO")))
|
||||
|
||||
USER_PASSWORD_RX = re_compile(r"^(?=.*?\p{Lowercase_Letter})(?=.*?\p{Uppercase_Letter})(?=.*?\d)(?=.*?[ !\"#$%&'()*+,./:;<=>?@[\\\]^_`{|}~-]).{8,}$")
|
||||
PLUGIN_NAME_RX = re_compile(r"^[\w.-]{4,64}$")
|
||||
|
||||
|
||||
def stop_gunicorn():
|
||||
p = Popen(["pgrep", "-f", "gunicorn"], stdout=PIPE)
|
||||
out, _ = p.communicate()
|
||||
pid = out.strip().decode().split("\n")[0]
|
||||
call(["kill", "-SIGTERM", pid])
|
||||
|
||||
|
||||
def stop(status, _stop=True):
|
||||
Path(sep, "var", "run", "bunkerweb", "ui.pid").unlink(missing_ok=True)
|
||||
TMP_DIR.joinpath("ui.healthy").unlink(missing_ok=True)
|
||||
if _stop is True:
|
||||
stop_gunicorn()
|
||||
_exit(status)
|
||||
|
||||
|
||||
def handle_stop(signum, frame):
|
||||
LOGGER.info("Caught stop operation")
|
||||
LOGGER.info("Stopping web ui ...")
|
||||
stop(0, False)
|
||||
|
||||
|
||||
def check_settings(settings: dict, check: str) -> bool:
|
||||
|
|
|
|||
Loading…
Reference in a new issue