mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
feat: add temporary UI service with logging and session management; update dependencies and Dockerfile
This commit is contained in:
parent
10c9f29f13
commit
f56524a35d
17 changed files with 550 additions and 71 deletions
|
|
@ -29,12 +29,21 @@ start() {
|
|||
export CAPTURE_OUTPUT="yes"
|
||||
# shellcheck disable=SC2046
|
||||
export $(cat /etc/bunkerweb/ui.env)
|
||||
sudo -E -u nginx -g nginx /bin/bash -c "PYTHONPATH=$PYTHONPATH python3 -m gunicorn --chdir /usr/share/bunkerweb/ui --config /usr/share/bunkerweb/ui/gunicorn.conf.py"
|
||||
sudo -E -u nginx -g nginx /bin/bash -c "PYTHONPATH=$PYTHONPATH python3 -m gunicorn --chdir /usr/share/bunkerweb/ui --logger-class utils.logger.TmpUiLogger --config utils/tmp-gunicorn.conf.py"
|
||||
sudo -E -u nginx -g nginx /bin/bash -c "PYTHONPATH=$PYTHONPATH python3 -m gunicorn --chdir /usr/share/bunkerweb/ui --logger-class utils.logger.UiLogger --config utils/gunicorn.conf.py"
|
||||
}
|
||||
|
||||
# Function to stop the UI
|
||||
stop() {
|
||||
echo "Stopping UI service..."
|
||||
|
||||
if [ -f "/var/run/bunkerweb/tmp-ui.pid" ]; then
|
||||
pid="$(cat /var/run/bunkerweb/tmp-ui.pid)"
|
||||
kill -s TERM "$pid"
|
||||
else
|
||||
echo "Temporary UI service is not running or the pid file doesn't exist."
|
||||
fi
|
||||
|
||||
if [ -f "/var/run/bunkerweb/ui.pid" ]; then
|
||||
pid="$(cat /var/run/bunkerweb/ui.pid)"
|
||||
kill -s TERM "$pid"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.13-alpine@sha256:fcbcbbecdeae71d3b77445d9144d1914df55110f825ab62b04a66c7c33c09373 AS builder
|
||||
FROM python:3.12-alpine@sha256:bb94273467caf397de28b4e6dd09ca4a2dd1b53fa9b130d5b2c7c82719258356 AS builder
|
||||
|
||||
# Export var for specific actions on linux/arm/v7
|
||||
ARG TARGETPLATFORM
|
||||
|
|
@ -35,7 +35,7 @@ COPY src/common/helpers helpers
|
|||
COPY src/VERSION VERSION
|
||||
COPY src/ui ui
|
||||
|
||||
FROM python:3.13-alpine@sha256:fcbcbbecdeae71d3b77445d9144d1914df55110f825ab62b04a66c7c33c09373
|
||||
FROM python:3.12-alpine@sha256:bb94273467caf397de28b4e6dd09ca4a2dd1b53fa9b130d5b2c7c82719258356
|
||||
|
||||
# Set default umask to prevent huge recursive chmod increasing the final image size
|
||||
RUN umask 027
|
||||
|
|
@ -73,7 +73,9 @@ RUN echo "Docker" > INTEGRATION && \
|
|||
find ui/ -type f -name "*.py" -print0 | xargs -0 chmod 750 && \
|
||||
chmod 660 INTEGRATION && \
|
||||
ln -s /proc/1/fd/1 /var/log/bunkerweb/ui-access.log && \
|
||||
ln -s /proc/1/fd/2 /var/log/bunkerweb/ui.log
|
||||
ln -s /proc/1/fd/2 /var/log/bunkerweb/ui.log && \
|
||||
ln -s /proc/1/fd/1 /var/log/bunkerweb/tmp-ui-access.log && \
|
||||
ln -s /proc/1/fd/2 /var/log/bunkerweb/tmp-ui.log
|
||||
|
||||
LABEL maintainer="Bunkerity <contact@bunkerity.com>"
|
||||
LABEL version="1.6.0-beta"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from app.dependencies import BW_INSTANCES_UTILS, DB
|
|||
home = Blueprint("home", __name__)
|
||||
|
||||
|
||||
@home.route("/home")
|
||||
@home.route("/home", methods=["GET"])
|
||||
@login_required
|
||||
def home_page():
|
||||
blocked_requests = BW_INSTANCES_UTILS.get_metrics("requests").get("requests", [])
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from datetime import datetime
|
||||
from os import getenv
|
||||
|
||||
from flask import Blueprint, flash as flask_flash, redirect, render_template, request, session, url_for
|
||||
from flask import Blueprint, current_app, flash as flask_flash, redirect, render_template, request, session, url_for
|
||||
from flask_login import current_user, login_user
|
||||
|
||||
from app.dependencies import DB
|
||||
|
|
@ -20,10 +20,14 @@ def login_page():
|
|||
|
||||
fail = False
|
||||
if request.method == "POST" and "username" in request.form and "password" in request.form:
|
||||
LOGGER.warning(f"Login attempt from {request.remote_addr}")
|
||||
LOGGER.warning(f"Login attempt from {request.remote_addr} with 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"]):
|
||||
# Regenerate the session to mitigate session fixation
|
||||
session.clear() # Clear the current session
|
||||
current_app.session_interface.regenerate(session) # Regenerate the session ID
|
||||
|
||||
# log the user in
|
||||
session["creation_date"] = datetime.now().astimezone()
|
||||
session["ip"] = request.remote_addr
|
||||
|
|
|
|||
10
src/ui/app/static/js/pages/starting.js
Normal file
10
src/ui/app/static/js/pages/starting.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
$(document).ready(() => {
|
||||
const $uiErrorMessage = $("#ui-error-message");
|
||||
|
||||
if (!$uiErrorMessage.length) {
|
||||
// Automatically refresh the page after 3 seconds
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
|
|
@ -99,11 +99,11 @@
|
|||
{% endif %}
|
||||
<!-- Page CSS -->
|
||||
<!-- Page -->
|
||||
{% if current_endpoint in ("setup", "login", "totp", "loading") %}
|
||||
{% if starting or current_endpoint in ("setup", "login", "totp", "loading") %}
|
||||
<link rel="stylesheet"
|
||||
href="{{ url_for('static', filename='css/pages/login.css') }}"
|
||||
nonce="{{ style_nonce }}" />
|
||||
{% elif current_endpoint == "loading" %}
|
||||
{% elif current_endpoint in ("loading", "starting") %}
|
||||
<link rel="stylesheet"
|
||||
href="{{ url_for('static', filename='css/pages/loading.css') }}"
|
||||
nonce="{{ style_nonce }}" />
|
||||
|
|
@ -124,7 +124,9 @@
|
|||
nonce="{{ script_nonce }}"></script>
|
||||
</head>
|
||||
<body>
|
||||
{% if not starting %}
|
||||
<input type="hidden" id="home-path" value="{{ url_for('home') }}" />
|
||||
{% endif %}
|
||||
<input type="hidden" id="is-read-only" value="{{ is_readonly }}" />
|
||||
<input type="hidden" id="theme" value="{{ theme }}" />
|
||||
<input type="hidden"
|
||||
|
|
@ -140,7 +142,7 @@
|
|||
id="avatar-url-white"
|
||||
value="{{ url_for('static', filename='img/avatar_profil_BW-white.png') }}" />
|
||||
<!-- prettier-ignore -->
|
||||
{% if current_endpoint != "loading" %}
|
||||
{% if not starting and current_endpoint != "loading" %}
|
||||
{% include "flash.html" %}
|
||||
{% endif %}
|
||||
{% block page %}{% endblock %}
|
||||
|
|
@ -179,7 +181,7 @@
|
|||
<script src="{{ url_for('static', filename='libs/ace/src-min/ace.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% endif %}
|
||||
{% if current_endpoint in ("setup", "loading", "instances") or current_endpoint != "plugins" and "plugins" in request.path %}
|
||||
{% if starting or current_endpoint in ("setup", "loading", "instances") or (current_endpoint != "plugins" and "plugins" in request.path) %}
|
||||
<script src="{{ url_for('static', filename='libs/lottie-player/lottie-player.min.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% endif %}
|
||||
|
|
@ -191,7 +193,7 @@
|
|||
nonce="{{ script_nonce }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/common.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% if current_endpoint in ("setup", "login", "totp", "loading") %}
|
||||
{% if starting or current_endpoint in ("setup", "login", "totp", "loading") %}
|
||||
<script src="{{ url_for('static', filename='js/pages/login.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% endif %}
|
||||
|
|
@ -208,6 +210,10 @@
|
|||
nonce="{{ script_nonce }}"></script>
|
||||
{% endif %}
|
||||
<!-- Page JS -->
|
||||
{% if starting %}
|
||||
<script src="{{ url_for('static', filename='js/pages/starting.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% endif %}
|
||||
{% if current_endpoint == "setup" %}
|
||||
<script src="{{ url_for('static', filename='js/pages/setup.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
|
|
@ -262,6 +268,6 @@
|
|||
<script src="{{ url_for('static', filename='js/pages/plugin_page.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% endif %}
|
||||
<script async defer src="{{ url_for('static', filename='js/buttons.js') }}"></script>
|
||||
<script async defer src="{{ url_for('static', filename='js/buttons.js') }}" nonce="{{ script_nonce }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
49
src/ui/app/templates/starting.html
Normal file
49
src/ui/app/templates/starting.html
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{% set starting = True %}
|
||||
{% extends "base.html" %}
|
||||
{% block page %}
|
||||
<div class="bg-{% if theme == 'light' %}light{% else %}dark{% endif %}-subtle">
|
||||
<div class="login-background d-flex justify-content-center">
|
||||
<div class="position-absolute top-0 p-3 w-100 w-lg-70">
|
||||
<div class="bg-bw-green position-relative w-100 p-2 text-white rounded fw-bold overflow-hidden">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="flex-grow-1 overflow-hidden me-2">
|
||||
<div id="banner-container">
|
||||
<p id="banner-text" class="mb-0 slide-in">
|
||||
Get the most of BunkerWeb by upgrading to the PRO version. More info and free trial <a class="light-href text-white-80"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href="https://panel.bunkerweb.io/?utm_campaign=self&utm_source=banner#pro">here</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<i id="next-news" role="button" class='bx bx-sm bx-chevron-right'></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-xxl">
|
||||
<div class="authentication-wrapper authentication-basic container-p-y">
|
||||
<div class="container-xxl d-flex justify-content-center align-items-center">
|
||||
<div class="authentication-inner">
|
||||
<div class="layout-main-wrapper mt-0 mb-0">
|
||||
<div class="layout-main-placeholder d-flex justify-content-center align-items-center">
|
||||
<lottie-player src="{{ url_for('static', filename='json/blockhaus.min.json') }}" background="transparent" speed="1" class="img-fluid" loop autoplay></lottie-player>
|
||||
</div>
|
||||
{% if message %}
|
||||
<div class="layout-main-info mt-1 mb-1">
|
||||
<h3 class="mb-0 don-jose">{{ message }}</h3>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if error %}
|
||||
<div id="ui-error-message" class="layout-main-info mt-1 mb-1">
|
||||
<h5 class="mb-0 don-jose text-danger">{{ error }}</h5>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Content -->
|
||||
{% endblock %}
|
||||
|
|
@ -81,18 +81,15 @@ COLUMNS_PREFERENCES_DEFAULTS = {
|
|||
}
|
||||
|
||||
|
||||
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()
|
||||
def stop(status, _stop: bool = True):
|
||||
if _stop:
|
||||
pid_file = Path(sep, "var", "run", "bunkerweb", "ui.pid")
|
||||
if pid_file.is_file():
|
||||
pid = pid_file.read_bytes()
|
||||
else:
|
||||
p = Popen(["pgrep", "-f", "gunicorn"], stdout=PIPE)
|
||||
pid, _ = p.communicate()
|
||||
call(["kill", "-SIGTERM", pid.strip().decode().split("\n")[0]])
|
||||
_exit(status)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,47 +1,99 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Load utility functions from a shared helper script.
|
||||
# shellcheck disable=SC1091
|
||||
. /usr/share/bunkerweb/helpers/utils.sh
|
||||
|
||||
# trap SIGTERM and SIGINT
|
||||
# Define a function to handle SIGTERM and SIGINT signals.
|
||||
function trap_exit() {
|
||||
# Log that the script caught a termination signal.
|
||||
# shellcheck disable=SC2317
|
||||
log "ENTRYPOINT" "ℹ️ " "Caught stop operation"
|
||||
|
||||
# Check if the temporary web UI process exists and stop it gracefully.
|
||||
# The PID is stored in a temporary file.
|
||||
# shellcheck disable=SC2317
|
||||
if [ -f "/var/run/bunkerweb/ui.pid" ] ; then
|
||||
if [ -f "/var/run/bunkerweb/tmp-ui.pid" ]; then
|
||||
log "ENTRYPOINT" "ℹ️ " "Stopping temporary web UI ..."
|
||||
|
||||
# Verify the process is running before attempting to stop it.
|
||||
if kill -0 "$(cat /var/run/bunkerweb/tmp-ui.pid)" 2> /dev/null; then
|
||||
# Send a TERM signal to stop the temporary web UI.
|
||||
kill -s TERM "$(cat /var/run/bunkerweb/tmp-ui.pid)"
|
||||
fi
|
||||
|
||||
# Log that the temporary web UI process has been stopped.
|
||||
log "ENTRYPOINT" "ℹ️ " "Temporary web UI stopped"
|
||||
fi
|
||||
|
||||
# Similarly, stop the main web UI process if it exists.
|
||||
# shellcheck disable=SC2317
|
||||
if [ -f "/var/run/bunkerweb/ui.pid" ]; then
|
||||
# shellcheck disable=SC2317
|
||||
log "ENTRYPOINT" "ℹ️ " "Stopping web UI ..."
|
||||
|
||||
# Verify the main web UI process is running before stopping it.
|
||||
# shellcheck disable=SC2317
|
||||
kill -s TERM "$(cat /var/run/bunkerweb/ui.pid)"
|
||||
if kill -0 "$(cat /var/run/bunkerweb/ui.pid)" 2> /dev/null; then
|
||||
# Send a TERM signal to stop the main web UI.
|
||||
# shellcheck disable=SC2317
|
||||
kill -s TERM "$(cat /var/run/bunkerweb/ui.pid)"
|
||||
fi
|
||||
|
||||
# Log that the main web UI process has been stopped.
|
||||
# shellcheck disable=SC2317
|
||||
log "ENTRYPOINT" "ℹ️ " "Web UI stopped"
|
||||
fi
|
||||
}
|
||||
|
||||
# Register the trap_exit function to handle SIGTERM, SIGINT, and SIGQUIT signals.
|
||||
trap "trap_exit" TERM INT QUIT
|
||||
|
||||
if [ -f /var/run/bunkerweb/ui.pid ] ; then
|
||||
# Remove any existing PID file for the main web UI to avoid stale state issues.
|
||||
if [ -f /var/run/bunkerweb/ui.pid ]; then
|
||||
rm -f /var/run/bunkerweb/ui.pid
|
||||
fi
|
||||
|
||||
# Log the startup of the web UI, including the version being launched.
|
||||
log "ENTRYPOINT" "ℹ️" "Starting the web UI v$(cat /usr/share/bunkerweb/VERSION) ..."
|
||||
|
||||
# setup and check /data folder
|
||||
# Set up and validate the /data folder, ensuring required configurations are present.
|
||||
/usr/share/bunkerweb/helpers/data.sh "ENTRYPOINT"
|
||||
|
||||
if [[ $(echo "$SWARM_MODE" | awk '{print tolower($0)}') == "yes" ]] ; then
|
||||
# Determine the deployment mode (Swarm, Kubernetes, Autoconf, or Docker) and record it.
|
||||
if [[ $(echo "$SWARM_MODE" | awk '{print tolower($0)}') == "yes" ]]; then
|
||||
echo "Swarm" > /usr/share/bunkerweb/INTEGRATION
|
||||
elif [[ $(echo "$KUBERNETES_MODE" | awk '{print tolower($0)}') == "yes" ]] ; then
|
||||
elif [[ $(echo "$KUBERNETES_MODE" | awk '{print tolower($0)}') == "yes" ]]; then
|
||||
echo "Kubernetes" > /usr/share/bunkerweb/INTEGRATION
|
||||
elif [[ $(echo "$AUTOCONF_MODE" | awk '{print tolower($0)}') == "yes" ]] ; then
|
||||
elif [[ $(echo "$AUTOCONF_MODE" | awk '{print tolower($0)}') == "yes" ]]; then
|
||||
echo "Autoconf" > /usr/share/bunkerweb/INTEGRATION
|
||||
else
|
||||
echo "Docker" > /usr/share/bunkerweb/INTEGRATION
|
||||
echo "Docker" > /usr/share/bunkerweb/INTEGRATION
|
||||
fi
|
||||
|
||||
python3 -m gunicorn --config gunicorn.conf.py
|
||||
# Start a temporary Gunicorn process with a special logger configuration.
|
||||
python3 -m gunicorn --logger-class utils.logger.TmpUiLogger --config utils/tmp-gunicorn.conf.py
|
||||
|
||||
if [ -f /var/tmp/bunkerweb/ui.healthy ] ; then
|
||||
rm /var/tmp/bunkerweb/ui.healthy
|
||||
# Start the main Gunicorn process with the standard logger configuration.
|
||||
python3 -m gunicorn --logger-class utils.logger.UiLogger --config utils/gunicorn.conf.py
|
||||
|
||||
# Capture the exit code of the main Gunicorn process.
|
||||
exit_code=$?
|
||||
|
||||
# Log the exit status of the main web UI process for debugging purposes.
|
||||
log "ENTRYPOINT" "ℹ️" "Web UI stopped with exit code $exit_code"
|
||||
|
||||
# If the temporary web UI process is still running, wait for it to stop.
|
||||
if [ -f "/var/run/bunkerweb/tmp-ui.pid" ]; then
|
||||
log "ENTRYPOINT" "ℹ️" "Waiting for temporary Web UI to stop ..."
|
||||
|
||||
# Wait in a loop until the temporary web UI process exits.
|
||||
while [ -f "/var/run/bunkerweb/tmp-ui.pid" ]; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
log "ENTRYPOINT" "ℹ️" "Temporary Web UI stopped"
|
||||
fi
|
||||
|
||||
log "ENTRYPOINT" "ℹ️" "Web UI stopped"
|
||||
exit 0
|
||||
# Exit the script with the same exit code as the main web UI process.
|
||||
exit $exit_code
|
||||
|
|
|
|||
|
|
@ -15,10 +15,12 @@ for deps_path in [join(sep, "usr", "share", "bunkerweb", *paths) for paths in ((
|
|||
if deps_path not in sys_path:
|
||||
sys_path.append(deps_path)
|
||||
|
||||
from cachelib import FileSystemCache
|
||||
from flask import Flask, Response, flash as flask_flash, jsonify, make_response, redirect, render_template, request, session, url_for
|
||||
from flask_executor import Executor
|
||||
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_session import Session
|
||||
from flask_wtf.csrf import CSRFProtect, CSRFError
|
||||
from werkzeug.routing.exceptions import BuildError
|
||||
|
||||
|
|
@ -77,25 +79,28 @@ with app.app_context():
|
|||
app.config["CHECK_PRIVATE_IP"] = getenv("CHECK_PRIVATE_IP", "yes").lower() == "yes"
|
||||
app.config["SECRET_KEY"] = FLASK_SECRET
|
||||
|
||||
app.config["SESSION_COOKIE_NAME"] = "__Host-bw_ui_session"
|
||||
app.config["SESSION_COOKIE_PATH"] = "/"
|
||||
app.config["SESSION_COOKIE_SECURE"] = True
|
||||
app.config["SESSION_COOKIE_PARTITIONED"] = True
|
||||
app.config["SESSION_COOKIE_HTTPONLY"] = True
|
||||
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
|
||||
|
||||
app.config["REMEMBER_COOKIE_NAME"] = "__Host-bw_ui_remember_token"
|
||||
app.config["REMEMBER_COOKIE_PATH"] = "/"
|
||||
app.config["REMEMBER_COOKIE_SECURE"] = True
|
||||
app.config["REMEMBER_COOKIE_HTTPONLY"] = True
|
||||
app.config["REMEMBER_COOKIE_SAMESITE"] = "Lax"
|
||||
|
||||
app.config["WTF_CSRF_SSL_STRICT"] = False
|
||||
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 86400
|
||||
app.config["MAX_CONTENT_LENGTH"] = 50 * 1024 * 1024 # 50 MB
|
||||
app.config["SCRIPT_NONCE"] = ""
|
||||
|
||||
app.config["EXECUTOR_MAX_WORKERS"] = 4
|
||||
# Session management
|
||||
app.config["SESSION_TYPE"] = "cachelib"
|
||||
app.config["SESSION_ID_LENGTH"] = 64
|
||||
app.config["SESSION_CACHELIB"] = FileSystemCache(threshold=500, cache_dir=LIB_DIR.joinpath("ui_sessions_cache"))
|
||||
sess = Session()
|
||||
sess.init_app(app)
|
||||
|
||||
# CSRF protection
|
||||
csrf = CSRFProtect()
|
||||
csrf.init_app(app)
|
||||
|
||||
principal = Principal()
|
||||
principal.init_app(app)
|
||||
|
|
@ -131,11 +136,8 @@ with app.app_context():
|
|||
url_for=custom_url_for,
|
||||
)
|
||||
|
||||
# CSRF protection
|
||||
csrf = CSRFProtect()
|
||||
csrf.init_app(app)
|
||||
|
||||
# Executor
|
||||
app.config["EXECUTOR_MAX_WORKERS"] = 4
|
||||
executor = Executor(app)
|
||||
|
||||
|
||||
|
|
@ -319,6 +321,20 @@ def before_request():
|
|||
response.headers["Retry-After"] = 30 # Clients should retry after 30 seconds # type: ignore
|
||||
return response
|
||||
|
||||
if request.environ.get("HTTP_X_FORWARDED_FOR") is not None:
|
||||
# Requests from the reverse proxy
|
||||
app.config["SESSION_COOKIE_NAME"] = "__Host-bw_ui_session"
|
||||
app.config["SESSION_COOKIE_SECURE"] = True
|
||||
app.config["REMEMBER_COOKIE_NAME"] = "__Host-bw_ui_remember_token"
|
||||
app.config["REMEMBER_COOKIE_SECURE"] = True
|
||||
else:
|
||||
# Requests from other sources
|
||||
app.config["SESSION_COOKIE_NAME"] = "bw_ui_session"
|
||||
app.config["SESSION_COOKIE_SECURE"] = False
|
||||
app.config["REMEMBER_COOKIE_NAME"] = "bw_ui_remember_token"
|
||||
app.config["REMEMBER_COOKIE_SECURE"] = False
|
||||
app.config["WTF_CSRF_SSL_STRICT"] = False
|
||||
|
||||
app.config["SCRIPT_NONCE"] = token_urlsafe(32)
|
||||
|
||||
if not request.path.startswith(("/css/", "/img/", "/js/", "/json/", "/fonts/", "/libs/")):
|
||||
|
|
@ -372,8 +388,8 @@ def set_security_headers(response):
|
|||
response.headers["Content-Security-Policy"] = (
|
||||
"object-src 'none';"
|
||||
+ " frame-ancestors 'self';"
|
||||
+ " default-src 'self' https://www.bunkerweb.io https://assets.bunkerity.com https://bunkerity.us1.list-manage.com https://api.github.com;"
|
||||
+ f" script-src 'self' 'nonce-{app.config['SCRIPT_NONCE']}';"
|
||||
+ " default-src https: http: 'self' https://www.bunkerweb.io https://assets.bunkerity.com https://bunkerity.us1.list-manage.com https://api.github.com;"
|
||||
+ f" script-src https: http: 'self' 'nonce-{app.config['SCRIPT_NONCE']}' 'strict-dynamic' 'unsafe-inline';"
|
||||
+ " style-src 'self' 'unsafe-inline';"
|
||||
+ " img-src 'self' data: blob: https://assets.bunkerity.com https://*.tile.openstreetmap.org;"
|
||||
+ " font-src 'self' data:;"
|
||||
|
|
@ -382,7 +398,7 @@ def set_security_headers(response):
|
|||
+ (
|
||||
" connect-src *;"
|
||||
if request.path.startswith(("/check", "/setup"))
|
||||
else " connect-src 'self' https://api.github.com/repos/bunkerity/bunkerweb https://www.bunkerweb.io/api/posts/0/3;"
|
||||
else " connect-src https: http: 'self' https://api.github.com/repos/bunkerity/bunkerweb https://www.bunkerweb.io/api/posts/0/3;"
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ Flask==3.1.0
|
|||
Flask-executor==1.0.0
|
||||
Flask-Login==0.6.3
|
||||
Flask-Principal==0.4.0
|
||||
Flask-Session[cachelib]==0.8.0
|
||||
Flask-WTF==1.2.2
|
||||
gunicorn[gthread]==23.0.0
|
||||
python-magic==0.4.27
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ blinker==1.9.0 \
|
|||
# via
|
||||
# flask
|
||||
# flask-principal
|
||||
cachelib==0.13.0 \
|
||||
--hash=sha256:209d8996e3c57595bee274ff97116d1d73c4980b2fd9a34c7846cd07fd2e1a48 \
|
||||
--hash=sha256:8c8019e53b6302967d4e8329a504acf75e7bc46130291d30188a6e4e58162516
|
||||
# via flask-session
|
||||
click==8.1.7 \
|
||||
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
|
||||
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
|
||||
|
|
@ -53,6 +57,7 @@ flask==3.1.0 \
|
|||
# flask-executor
|
||||
# flask-login
|
||||
# flask-principal
|
||||
# flask-session
|
||||
# flask-wtf
|
||||
flask-executor==1.0.0 \
|
||||
--hash=sha256:4bc113def5d9f1c7ff272ff7ba09f843468eab80469bdbd21625a111b2c8ae7b \
|
||||
|
|
@ -65,6 +70,12 @@ flask-login==0.6.3 \
|
|||
flask-principal==0.4.0 \
|
||||
--hash=sha256:f5d6134b5caebfdbb86f32d56d18ee44b080876a27269560a96ea35f75c99453
|
||||
# via -r requirements.in
|
||||
flask-session==0.8.0 \
|
||||
--hash=sha256:20e045eb01103694e70be4a49f3a80dbb1b57296a22dc6f44bbf3f83ef0742ff \
|
||||
--hash=sha256:5dae6e9ddab334f8dc4dea4305af37851f4e7dc0f484caf3351184001195e3b7
|
||||
# via
|
||||
# -r requirements.in
|
||||
# flask-session
|
||||
flask-wtf==1.2.2 \
|
||||
--hash=sha256:79d2ee1e436cf570bccb7d916533fa18757a2f18c290accffab1b9a0b684666b \
|
||||
--hash=sha256:e93160c5c5b6b571cf99300b6e01b72f9a101027cab1579901f8b10c5daf0b70
|
||||
|
|
@ -153,6 +164,44 @@ markupsafe==3.0.2 \
|
|||
# jinja2
|
||||
# werkzeug
|
||||
# wtforms
|
||||
msgspec==0.18.6 \
|
||||
--hash=sha256:06acbd6edf175bee0e36295d6b0302c6de3aaf61246b46f9549ca0041a9d7177 \
|
||||
--hash=sha256:0e24539b25c85c8f0597274f11061c102ad6b0c56af053373ba4629772b407be \
|
||||
--hash=sha256:1003c20bfe9c6114cc16ea5db9c5466e49fae3d7f5e2e59cb70693190ad34da0 \
|
||||
--hash=sha256:1a76b60e501b3932782a9da039bd1cd552b7d8dec54ce38332b87136c64852dd \
|
||||
--hash=sha256:37f67c1d81272131895bb20d388dd8d341390acd0e192a55ab02d4d6468b434c \
|
||||
--hash=sha256:3ac4dd63fd5309dd42a8c8c36c1563531069152be7819518be0a9d03be9788e4 \
|
||||
--hash=sha256:40a4df891676d9c28a67c2cc39947c33de516335680d1316a89e8f7218660410 \
|
||||
--hash=sha256:41cf758d3f40428c235c0f27bc6f322d43063bc32da7b9643e3f805c21ed57b4 \
|
||||
--hash=sha256:46eb2f6b22b0e61c137e65795b97dc515860bf6ec761d8fb65fdb62aa094ba61 \
|
||||
--hash=sha256:6aa85198f8f154cf35d6f979998f6dadd3dc46a8a8c714632f53f5d65b315c07 \
|
||||
--hash=sha256:7481355a1adcf1f08dedd9311193c674ffb8bf7b79314b4314752b89a2cf7f1c \
|
||||
--hash=sha256:77f30b0234eceeff0f651119b9821ce80949b4d667ad38f3bfed0d0ebf9d6d8f \
|
||||
--hash=sha256:9080eb12b8f59e177bd1eb5c21e24dd2ba2fa88a1dbc9a98e05ad7779b54c681 \
|
||||
--hash=sha256:974d3520fcc6b824a6dedbdf2b411df31a73e6e7414301abac62e6b8d03791b4 \
|
||||
--hash=sha256:9da21f804c1a1471f26d32b5d9bc0480450ea77fbb8d9db431463ab64aaac2cf \
|
||||
--hash=sha256:a59fc3b4fcdb972d09138cb516dbde600c99d07c38fd9372a6ef500d2d031b4e \
|
||||
--hash=sha256:a6896f4cd5b4b7d688018805520769a8446df911eb93b421c6c68155cdf9dd5a \
|
||||
--hash=sha256:ad237100393f637b297926cae1868b0d500f764ccd2f0623a380e2bcfb2809ca \
|
||||
--hash=sha256:b5c390b0b0b7da879520d4ae26044d74aeee5144f83087eb7842ba59c02bc090 \
|
||||
--hash=sha256:c3232fabacef86fe8323cecbe99abbc5c02f7698e3f5f2e248e3480b66a3596b \
|
||||
--hash=sha256:c61ee4d3be03ea9cd089f7c8e36158786cd06e51fbb62529276452bbf2d52ece \
|
||||
--hash=sha256:c8355b55c80ac3e04885d72db515817d9fbb0def3bab936bba104e99ad22cf46 \
|
||||
--hash=sha256:cc001cf39becf8d2dcd3f413a4797c55009b3a3cdbf78a8bf5a7ca8fdb76032c \
|
||||
--hash=sha256:ce13981bfa06f5eb126a3a5a38b1976bddb49a36e4f46d8e6edecf33ccf11df1 \
|
||||
--hash=sha256:d0feb7a03d971c1c0353de1a8fe30bb6579c2dc5ccf29b5f7c7ab01172010492 \
|
||||
--hash=sha256:d5351afb216b743df4b6b147691523697ff3a2fc5f3d54f771e91219f5c23aaa \
|
||||
--hash=sha256:d70cb3d00d9f4de14d0b31d38dfe60c88ae16f3182988246a9861259c6722af6 \
|
||||
--hash=sha256:d86f5071fe33e19500920333c11e2267a31942d18fed4d9de5bc2fbab267d28c \
|
||||
--hash=sha256:db1d8626748fa5d29bbd15da58b2d73af25b10aa98abf85aab8028119188ed57 \
|
||||
--hash=sha256:e3b524df6ea9998bbc99ea6ee4d0276a101bcc1aa8d14887bb823914d9f60d07 \
|
||||
--hash=sha256:e77e56ffe2701e83a96e35770c6adb655ffc074d530018d1b584a8e635b4f36f \
|
||||
--hash=sha256:e97dec6932ad5e3ee1e3c14718638ba333befc45e0661caa57033cd4cc489466 \
|
||||
--hash=sha256:f7d9faed6dfff654a9ca7d9b0068456517f63dbc3aa704a527f493b9200b210a \
|
||||
--hash=sha256:fac5834e14ac4da1fca373753e0c4ec9c8069d1fe5f534fa5208453b6065d5be \
|
||||
--hash=sha256:fd62e5818731a66aaa8e9b0a1e5543dc979a46278da01e85c3c9a1a4f047ef7e \
|
||||
--hash=sha256:fda4c357145cf0b760000c4ad597e19b53adf01382b711f281720a10a0fe72b7
|
||||
# via flask-session
|
||||
packaging==24.2 \
|
||||
--hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \
|
||||
--hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f
|
||||
|
|
@ -265,9 +314,9 @@ regex==2024.11.6 \
|
|||
--hash=sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9 \
|
||||
--hash=sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91
|
||||
# via -r requirements.in
|
||||
six==1.16.0 \
|
||||
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
|
||||
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
|
||||
six==1.17.0 \
|
||||
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
|
||||
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
|
||||
# via python-dateutil
|
||||
soupsieve==2.6 \
|
||||
--hash=sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb \
|
||||
|
|
|
|||
107
src/ui/temp.py
Normal file
107
src/ui/temp.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
from os import _exit, getenv, sep
|
||||
from os.path import join
|
||||
from pathlib import Path
|
||||
from secrets import token_urlsafe
|
||||
from signal import SIGINT, SIGTERM, signal
|
||||
from subprocess import PIPE, Popen, call
|
||||
from sys import path as sys_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:
|
||||
sys_path.append(deps_path)
|
||||
|
||||
from flask import Flask, render_template, request
|
||||
|
||||
from logger import setup_logger # type: ignore
|
||||
|
||||
LOGGER = setup_logger("TMP-UI", getenv("CUSTOM_LOG_LEVEL", getenv("LOG_LEVEL", "INFO")))
|
||||
|
||||
TMP_DIR = Path(sep, "var", "tmp", "bunkerweb")
|
||||
ERROR_FILE = TMP_DIR.joinpath("ui.error")
|
||||
|
||||
|
||||
def stop(status):
|
||||
pid_file = Path(sep, "var", "run", "bunkerweb", "tmp-ui.pid")
|
||||
if pid_file.is_file():
|
||||
pid = pid_file.read_bytes()
|
||||
else:
|
||||
p = Popen(["pgrep", "-f", "gunicorn"], stdout=PIPE)
|
||||
pid, _ = p.communicate()
|
||||
call(["kill", "-SIGTERM", pid.strip().decode().split("\n")[0]])
|
||||
_exit(status)
|
||||
|
||||
|
||||
def handle_stop(signum, frame):
|
||||
LOGGER.info("Caught stop operation")
|
||||
LOGGER.info("Stopping web ui ...")
|
||||
stop(0, False)
|
||||
|
||||
|
||||
signal(SIGINT, handle_stop)
|
||||
signal(SIGTERM, handle_stop)
|
||||
|
||||
app = Flask(__name__, static_url_path="/", static_folder="app/static", template_folder="app/templates")
|
||||
app.url_map.strict_slashes = False
|
||||
|
||||
app.config["SCRIPT_NONCE"] = ""
|
||||
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
app.config["SCRIPT_NONCE"] = token_urlsafe(32)
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def inject_variables():
|
||||
return dict(
|
||||
current_endpoint=request.path.split("/")[-1],
|
||||
script_nonce=app.config["SCRIPT_NONCE"],
|
||||
)
|
||||
|
||||
|
||||
@app.after_request
|
||||
def set_security_headers(response):
|
||||
"""Set the security headers."""
|
||||
# * Content-Security-Policy header to prevent XSS attacks
|
||||
response.headers["Content-Security-Policy"] = (
|
||||
"object-src 'none';"
|
||||
+ " frame-ancestors 'self';"
|
||||
+ " default-src 'self'"
|
||||
+ f" script-src https: http: 'self' 'nonce-{app.config['SCRIPT_NONCE']}' 'strict-dynamic' 'unsafe-inline';"
|
||||
+ " style-src 'self' 'unsafe-inline';"
|
||||
+ " img-src 'self' data: blob: https://assets.bunkerity.com;"
|
||||
+ " base-uri 'self';"
|
||||
+ " block-all-mixed-content;"
|
||||
)
|
||||
|
||||
# * X-Frames-Options header to prevent clickjacking
|
||||
response.headers["X-Frame-Options"] = "DENY"
|
||||
|
||||
# * X-Content-Type-Options header to prevent MIME sniffing
|
||||
response.headers["X-Content-Type-Options"] = "nosniff"
|
||||
|
||||
# * Referrer-Policy header to prevent leaking of sensitive data
|
||||
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found_handler(error):
|
||||
message = "BunkerWeb UI is starting..."
|
||||
error = ""
|
||||
if ERROR_FILE.is_file():
|
||||
message = "BunkerWeb UI encountered an error while starting."
|
||||
error = ERROR_FILE.read_text()
|
||||
return render_template("starting.html", message=message, error=error)
|
||||
|
||||
|
||||
@app.route("/", defaults={"path": ""})
|
||||
@app.route("/<path:path>")
|
||||
def catch_all(path):
|
||||
message = "BunkerWeb UI is starting..."
|
||||
error = ""
|
||||
if ERROR_FILE.is_file():
|
||||
message = "BunkerWeb UI encountered an error while starting."
|
||||
error = ERROR_FILE.read_text()
|
||||
return render_template("starting.html", message=message, error=error)
|
||||
0
src/ui/utils/__init__.py
Normal file
0
src/ui/utils/__init__.py
Normal file
|
|
@ -6,8 +6,10 @@ from os.path import join
|
|||
from pathlib import Path
|
||||
from secrets import token_hex
|
||||
from stat import S_IRUSR, S_IWUSR
|
||||
from subprocess import call
|
||||
from sys import exit, path as sys_path
|
||||
from time import sleep
|
||||
from traceback import format_exc
|
||||
|
||||
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:
|
||||
|
|
@ -26,9 +28,13 @@ LIB_DIR = Path(sep, "var", "lib", "bunkerweb")
|
|||
|
||||
UI_DATA_FILE = TMP_DIR.joinpath("ui_data.json")
|
||||
HEALTH_FILE = TMP_DIR.joinpath("ui.healthy")
|
||||
ERROR_FILE = TMP_DIR.joinpath("ui.error")
|
||||
|
||||
TMP_PID_FILE = RUN_DIR.joinpath("tmp-ui.pid")
|
||||
PID_FILE = RUN_DIR.joinpath("ui.pid")
|
||||
|
||||
UI_SESSIONS_CACHE = LIB_DIR.joinpath("ui_sessions_cache")
|
||||
|
||||
FLASK_SECRET_FILE = LIB_DIR.joinpath(".flask_secret")
|
||||
FLASK_SECRET_HASH_FILE = FLASK_SECRET_FILE.with_suffix(".hash") # File to store hash of Flask secret
|
||||
TOTP_SECRETS_FILE = LIB_DIR.joinpath(".totp_secrets.json")
|
||||
|
|
@ -51,6 +57,7 @@ limit_request_line = 0
|
|||
limit_request_fields = 32768
|
||||
limit_request_field_size = 0
|
||||
reuse_port = True
|
||||
daemon = False
|
||||
chdir = join(sep, "usr", "share", "bunkerweb", "ui")
|
||||
umask = 0x027
|
||||
pidfile = PID_FILE.as_posix()
|
||||
|
|
@ -86,6 +93,8 @@ def on_starting(server):
|
|||
RUN_DIR.mkdir(parents=True, exist_ok=True)
|
||||
LIB_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
ERROR_FILE.unlink(missing_ok=True)
|
||||
|
||||
LOGGER = setup_logger("UI", getenv("CUSTOM_LOG_LEVEL", getenv("LOG_LEVEL", "INFO")))
|
||||
|
||||
def set_secure_permissions(file_path: Path):
|
||||
|
|
@ -137,9 +146,12 @@ def on_starting(server):
|
|||
set_secure_permissions(FLASK_SECRET_HASH_FILE)
|
||||
|
||||
LOGGER.info("Flask secret securely stored.")
|
||||
LOGGER.info("Flask secret hash stored for change detection.")
|
||||
LOGGER.info("Flask secret hash securely stored for change detection.")
|
||||
except Exception as e:
|
||||
LOGGER.critical(f"An error occurred while handling the Flask secret: {e}")
|
||||
message = f"An error occurred while handling the Flask secret: {e}"
|
||||
LOGGER.debug(format_exc())
|
||||
LOGGER.critical(message)
|
||||
ERROR_FILE.write_text(message, encoding="utf-8")
|
||||
exit(1)
|
||||
|
||||
# * Handle TOTP secrets
|
||||
|
|
@ -201,15 +213,25 @@ def on_starting(server):
|
|||
set_secure_permissions(TOTP_HASH_FILE)
|
||||
|
||||
LOGGER.info("TOTP secrets securely stored.")
|
||||
LOGGER.info("TOTP environment hash stored for change detection.")
|
||||
LOGGER.info("TOTP environment hash securely stored for change detection.")
|
||||
except Exception as e:
|
||||
LOGGER.critical(f"An error occurred while handling TOTP secrets: {e}")
|
||||
message = f"An error occurred while handling TOTP secrets: {e}"
|
||||
LOGGER.debug(format_exc())
|
||||
LOGGER.critical(message)
|
||||
ERROR_FILE.write_text(message, encoding="utf-8")
|
||||
exit(1)
|
||||
|
||||
DB = UIDatabase(LOGGER)
|
||||
current_time = datetime.now().astimezone()
|
||||
|
||||
ready = False
|
||||
while not ready:
|
||||
if (datetime.now().astimezone() - current_time).total_seconds() > 60:
|
||||
message = "Timed out while waiting for the database to be initialized."
|
||||
LOGGER.error(message)
|
||||
ERROR_FILE.write_text(message, encoding="utf-8")
|
||||
exit(1)
|
||||
|
||||
db_metadata = DB.get_metadata()
|
||||
ui_roles = DB.get_ui_roles(as_dict=True)
|
||||
if isinstance(db_metadata, str) or not db_metadata["is_initialized"] or (isinstance(ui_roles, str) and "doesn't exist" in ui_roles):
|
||||
|
|
@ -222,21 +244,35 @@ def on_starting(server):
|
|||
if not ui_roles:
|
||||
ret = DB.create_ui_role("admin", "Admins can create new users, edit and read the data.", ["manage", "write", "read"])
|
||||
if ret:
|
||||
LOGGER.error(f"Couldn't create the admin role in the database: {ret}")
|
||||
message = f"Couldn't create the admin role in the database: {ret}"
|
||||
LOGGER.error(message)
|
||||
ERROR_FILE.write_text(message, encoding="utf-8")
|
||||
exit(1)
|
||||
|
||||
ret = DB.create_ui_role("writer", "Writers can edit and read the data but can't create new users.", ["write", "read"])
|
||||
if ret:
|
||||
LOGGER.error(f"Couldn't create the admin role in the database: {ret}")
|
||||
message = f"Couldn't create the writer role in the database: {ret}"
|
||||
LOGGER.error(message)
|
||||
ERROR_FILE.write_text(message, encoding="utf-8")
|
||||
exit(1)
|
||||
|
||||
ret = DB.create_ui_role("reader", "Readers can only read the data.", ["read"])
|
||||
if ret:
|
||||
LOGGER.error(f"Couldn't create the admin role in the database: {ret}")
|
||||
message = f"Couldn't create the reader role in the database: {ret}"
|
||||
LOGGER.error(message)
|
||||
ERROR_FILE.write_text(message, encoding="utf-8")
|
||||
exit(1)
|
||||
|
||||
current_time = datetime.now().astimezone()
|
||||
|
||||
ADMIN_USER = "Error"
|
||||
while ADMIN_USER == "Error":
|
||||
if (datetime.now().astimezone() - current_time).total_seconds() > 60:
|
||||
message = "Timed out while waiting for the admin user."
|
||||
LOGGER.error(message)
|
||||
ERROR_FILE.write_text(message, encoding="utf-8")
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
ADMIN_USER = DB.get_ui_user(as_dict=True)
|
||||
except BaseException as e:
|
||||
|
|
@ -277,7 +313,7 @@ def on_starting(server):
|
|||
if err:
|
||||
LOGGER.error(f"Couldn't update the admin user in the database: {err}")
|
||||
else:
|
||||
LOGGER.info("The admin user was updated successfully")
|
||||
LOGGER.info("The admin user was updated successfully.")
|
||||
else:
|
||||
LOGGER.warning("The admin user wasn't created manually. You can't change it from the environment variables.")
|
||||
elif env_admin_username and env_admin_password:
|
||||
|
|
@ -285,17 +321,21 @@ def on_starting(server):
|
|||
|
||||
if not DEBUG:
|
||||
if len(user_name) > 256:
|
||||
LOGGER.error("The admin username is too long. It must be less than 256 characters.")
|
||||
message = "The admin username is too long. It must be less than 256 characters."
|
||||
LOGGER.error(message)
|
||||
ERROR_FILE.write_text(message, encoding="utf-8")
|
||||
exit(1)
|
||||
elif not USER_PASSWORD_RX.match(env_admin_password):
|
||||
LOGGER.error(
|
||||
"The admin password is not strong enough. It must contain at least 8 characters, including at least 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character (#@?!$%^&*-)."
|
||||
)
|
||||
message = "The admin password is not strong enough. It must contain at least 8 characters, including at least 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character (#@?!$%^&*-)."
|
||||
LOGGER.error(message)
|
||||
ERROR_FILE.write_text(message, encoding="utf-8")
|
||||
exit(1)
|
||||
|
||||
ret = DB.create_ui_user(user_name, gen_password_hash(env_admin_password), ["admin"], admin=True)
|
||||
if ret and "already exists" not in ret:
|
||||
LOGGER.error(f"Couldn't create the admin user in the database: {ret}")
|
||||
message = f"Couldn't create the admin user in the database: {ret}"
|
||||
LOGGER.critical(message)
|
||||
ERROR_FILE.write_text(message, encoding="utf-8")
|
||||
exit(1)
|
||||
|
||||
latest_release = None
|
||||
|
|
@ -325,9 +365,24 @@ def on_starting(server):
|
|||
|
||||
LOGGER.info(
|
||||
"UI will disconnect users that have their IP address changed during a session"
|
||||
+ (" except for private IP addresses" if getenv("CHECK_PRIVATE_IP", "yes").lower() == "no" else "")
|
||||
+ (" except for private IP addresses." if getenv("CHECK_PRIVATE_IP", "yes").lower() == "no" else ".")
|
||||
)
|
||||
|
||||
UI_SESSIONS_CACHE.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if TMP_PID_FILE.is_file():
|
||||
LOGGER.info("Stopping temporary UI...")
|
||||
call(["kill", "-SIGQUIT", TMP_PID_FILE.read_text().strip()])
|
||||
current_time = datetime.now().astimezone()
|
||||
while TMP_PID_FILE.is_file():
|
||||
if (datetime.now().astimezone() - current_time).total_seconds() > 60:
|
||||
message = "Timed out while waiting for the temporary UI to stop."
|
||||
LOGGER.error(message)
|
||||
ERROR_FILE.write_text(message, encoding="utf-8")
|
||||
exit(1)
|
||||
sleep(1)
|
||||
LOGGER.info("Temporary UI is stopped")
|
||||
|
||||
LOGGER.info("UI is ready")
|
||||
|
||||
|
||||
42
src/ui/utils/logger.py
Normal file
42
src/ui/utils/logger.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
from logging import getLogger
|
||||
from os.path import join, sep
|
||||
from sys import path as sys_path
|
||||
from threading import Lock
|
||||
|
||||
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)
|
||||
|
||||
from gunicorn.glogging import Logger
|
||||
|
||||
from logger import DATE_FORMAT, LOG_FORMAT # type: ignore
|
||||
|
||||
|
||||
class UiLogger(Logger):
|
||||
|
||||
error_log = None
|
||||
access_log = None
|
||||
|
||||
error_fmt = LOG_FORMAT
|
||||
datefmt = DATE_FORMAT
|
||||
|
||||
def __init__(self, cfg):
|
||||
if not self.error_log:
|
||||
self.error_log = getLogger("UI")
|
||||
self.error_log.propagate = False
|
||||
if not self.access_log:
|
||||
self.access_log = getLogger("UI.access")
|
||||
self.access_log.propagate = False
|
||||
self.error_handlers = []
|
||||
self.access_handlers = []
|
||||
self.logfile = None
|
||||
self.lock = Lock()
|
||||
self.cfg = cfg
|
||||
self.setup(cfg)
|
||||
|
||||
|
||||
class TmpUiLogger(UiLogger):
|
||||
def __init__(self, cfg):
|
||||
self.error_log = getLogger("TMP-UI")
|
||||
self.access_log = getLogger("TMP-UI.access")
|
||||
super().__init__(cfg)
|
||||
80
src/ui/utils/tmp-gunicorn.conf.py
Normal file
80
src/ui/utils/tmp-gunicorn.conf.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
from os import getenv, sep
|
||||
from os.path import join
|
||||
from pathlib import Path
|
||||
from sys import path as sys_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:
|
||||
sys_path.append(deps_path)
|
||||
|
||||
from logger import setup_logger # type: ignore
|
||||
|
||||
TMP_DIR = Path(sep, "var", "tmp", "bunkerweb")
|
||||
RUN_DIR = Path(sep, "var", "run", "bunkerweb")
|
||||
LIB_DIR = Path(sep, "var", "lib", "bunkerweb")
|
||||
|
||||
HEALTH_FILE = TMP_DIR.joinpath("tmp-ui.healthy")
|
||||
PID_FILE = RUN_DIR.joinpath("tmp-ui.pid")
|
||||
|
||||
LOG_LEVEL = getenv("CUSTOM_LOG_LEVEL", getenv("LOG_LEVEL", "info"))
|
||||
LISTEN_ADDR = getenv("LISTEN_ADDR", "0.0.0.0")
|
||||
LISTEN_PORT = getenv("LISTEN_PORT", "7000")
|
||||
FORWARDED_ALLOW_IPS = getenv("FORWARDED_ALLOW_IPS", "*")
|
||||
CAPTURE_OUTPUT = getenv("CAPTURE_OUTPUT", "no").lower() == "yes"
|
||||
|
||||
wsgi_app = "temp:app"
|
||||
proc_name = "bunkerweb-tmp-ui"
|
||||
accesslog = join(sep, "var", "log", "bunkerweb", "tmp-ui-access.log")
|
||||
access_log_format = '%({x-forwarded-for}i)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
|
||||
errorlog = join(sep, "var", "log", "bunkerweb", "tmp-ui.log")
|
||||
capture_output = CAPTURE_OUTPUT
|
||||
limit_request_line = 0
|
||||
limit_request_fields = 32768
|
||||
limit_request_field_size = 0
|
||||
reuse_port = True
|
||||
daemon = True
|
||||
chdir = join(sep, "usr", "share", "bunkerweb", "ui")
|
||||
umask = 0x027
|
||||
pidfile = PID_FILE.as_posix()
|
||||
worker_tmp_dir = join(sep, "dev", "shm")
|
||||
tmp_upload_dir = join(sep, "var", "tmp", "bunkerweb", "ui")
|
||||
secure_scheme_headers = {}
|
||||
forwarded_allow_ips = FORWARDED_ALLOW_IPS
|
||||
pythonpath = join(sep, "usr", "share", "bunkerweb", "deps", "python") + "," + join(sep, "usr", "share", "bunkerweb", "ui")
|
||||
proxy_allow_ips = "*"
|
||||
casefold_http_method = True
|
||||
workers = 1
|
||||
bind = f"{LISTEN_ADDR}:{LISTEN_PORT}"
|
||||
worker_class = "gthread"
|
||||
threads = 2
|
||||
max_requests_jitter = 0
|
||||
graceful_timeout = 0
|
||||
|
||||
DEBUG = getenv("DEBUG", False)
|
||||
|
||||
loglevel = "debug" if DEBUG else LOG_LEVEL.lower()
|
||||
|
||||
if DEBUG:
|
||||
reload = True
|
||||
reload_extra_files = [
|
||||
file.as_posix()
|
||||
for file in Path(sep, "usr", "share", "bunkerweb", "ui", "app").rglob("*")
|
||||
if "__pycache__" not in file.parts and "static" not in file.parts
|
||||
]
|
||||
|
||||
|
||||
def on_starting(server):
|
||||
TMP_DIR.mkdir(parents=True, exist_ok=True)
|
||||
RUN_DIR.mkdir(parents=True, exist_ok=True)
|
||||
LIB_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
LOGGER = setup_logger("TMP-UI", getenv("CUSTOM_LOG_LEVEL", getenv("LOG_LEVEL", "INFO")))
|
||||
LOGGER.info("TMP-UI is ready")
|
||||
|
||||
|
||||
def when_ready(server):
|
||||
HEALTH_FILE.write_text("ok", encoding="utf-8")
|
||||
|
||||
|
||||
def on_exit(server):
|
||||
HEALTH_FILE.unlink(missing_ok=True)
|
||||
Loading…
Reference in a new issue