e2f56b8cc8
Flask WebUI behind nginx reverse proxy with zone-based firewall, DHCP, WireGuard, and ACME certificate management.
225 lines
6.1 KiB
Python
225 lines
6.1 KiB
Python
"""
|
|
server.py - Vacuum Wall management WebUI entry point.
|
|
|
|
Serves the Flask application on 127.0.0.1:9090. Nginx terminates SSL
|
|
and enforces basic authentication before proxying to this port.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
from datetime import datetime
|
|
|
|
from flask import Flask, render_template
|
|
|
|
from lib.acme import get_email, list_certs
|
|
from lib.dnsmasq import get_config as dnsmasq_config
|
|
from lib.dnsmasq import get_lease_table
|
|
from lib.dnsmasq import get_status as dnsmasq_status
|
|
from lib.firewall import get_active_zones, get_interfaces, get_zone_info
|
|
from lib.nginx import get_config as nginx_config
|
|
from lib.nginx import get_domains
|
|
from lib.wireguard import get_config as wg_config
|
|
from lib.wireguard import status as wg_status
|
|
from webui.api.certs import bp as certs_bp
|
|
from webui.api.dhcp import bp as dhcp_bp
|
|
from webui.api.firewall import bp as firewall_bp
|
|
from webui.api.proxy import bp as proxy_bp
|
|
from webui.api.wireguard import bp as wireguard_bp
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# App factory
|
|
# ---------------------------------------------------------------------------
|
|
|
|
app = Flask(__name__)
|
|
app.config["SECRET_KEY"] = os.urandom(32).hex()
|
|
|
|
app.register_blueprint(firewall_bp, url_prefix="/api/firewall")
|
|
app.register_blueprint(dhcp_bp, url_prefix="/api/dhcp")
|
|
app.register_blueprint(proxy_bp, url_prefix="/api/proxy")
|
|
app.register_blueprint(certs_bp, url_prefix="/api/certs")
|
|
app.register_blueprint(wireguard_bp, url_prefix="/api/wireguard")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Jinja2 custom filters
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@app.template_filter("timestamp")
|
|
def timestamp_filter(value):
|
|
"""Convert an ISO timestamp string to a human-readable date."""
|
|
if not value:
|
|
return ""
|
|
try:
|
|
dt = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
except (ValueError, TypeError):
|
|
return str(value)
|
|
|
|
|
|
@app.template_filter("bytes")
|
|
def bytes_filter(value):
|
|
"""Format a byte count to a human-readable string (KB / MB / GB)."""
|
|
try:
|
|
num = float(value)
|
|
except (ValueError, TypeError):
|
|
return str(value)
|
|
if num < 0:
|
|
return "0 B"
|
|
for unit in ("B", "KB", "MB", "GB", "TB"):
|
|
if abs(num) < 1024:
|
|
return f"{num:.1f} {unit}"
|
|
num /= 1024
|
|
return f"{num:.1f} PB"
|
|
|
|
|
|
@app.template_filter("duration")
|
|
def duration_filter(value):
|
|
"""Format a duration in seconds to a human-readable string."""
|
|
try:
|
|
total = int(float(value))
|
|
except (ValueError, TypeError):
|
|
return str(value)
|
|
if total < 0:
|
|
return "0s"
|
|
parts = []
|
|
days, remainder = divmod(total, 86400)
|
|
hours, remainder = divmod(remainder, 3600)
|
|
minutes, seconds = divmod(remainder, 60)
|
|
if days:
|
|
parts.append(f"{days}d")
|
|
if hours:
|
|
parts.append(f"{hours}h")
|
|
if minutes:
|
|
parts.append(f"{minutes}m")
|
|
parts.append(f"{seconds}s")
|
|
return " ".join(parts)
|
|
|
|
|
|
@app.template_filter("json_pretty")
|
|
def json_pretty_filter(value):
|
|
"""Pretty-print a JSON-serialisable value for debug displays."""
|
|
import json
|
|
|
|
try:
|
|
return json.dumps(value, indent=2, default=str)
|
|
except (TypeError, ValueError):
|
|
return str(value)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Page routes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _safely(fn, default=None):
|
|
"""Call *fn* and return *default* on any exception."""
|
|
try:
|
|
return fn()
|
|
except Exception as exc:
|
|
logger.warning("WebUI data load failed: %s", exc)
|
|
return default
|
|
|
|
|
|
@app.route("/")
|
|
def dashboard():
|
|
active_zones = _safely(get_active_zones, {})
|
|
interfaces = _safely(get_interfaces, [])
|
|
dnsmasq = _safely(dnsmasq_status, {})
|
|
domains = _safely(get_domains, [])
|
|
certs = _safely(list_certs, [])
|
|
wg = _safely(wg_status, {})
|
|
|
|
return render_template(
|
|
"dashboard.html",
|
|
active_zones=active_zones,
|
|
interfaces=interfaces,
|
|
dnsmasq=dnsmasq,
|
|
domains=domains,
|
|
certs=certs,
|
|
wg_status=wg,
|
|
)
|
|
|
|
|
|
@app.route("/interfaces")
|
|
def interfaces_page():
|
|
return render_template(
|
|
"interfaces.html",
|
|
interfaces=_safely(get_interfaces, []),
|
|
active_zones=_safely(get_active_zones, {}),
|
|
)
|
|
|
|
|
|
@app.route("/zones")
|
|
def zones_page():
|
|
zones = {}
|
|
for name in _safely(get_active_zones, {}):
|
|
zones[name] = _safely(lambda n=name: get_zone_info(n), {})
|
|
return render_template(
|
|
"zones.html",
|
|
zones=zones,
|
|
interfaces=_safely(get_interfaces, []),
|
|
services=_safely(
|
|
lambda: __import__(
|
|
"lib.firewall", fromlist=["get_services"]
|
|
).get_services(),
|
|
[],
|
|
),
|
|
)
|
|
|
|
|
|
@app.route("/rules")
|
|
def rules_page():
|
|
zones = list(_safely(get_active_zones, {}).keys())
|
|
return render_template("rules.html", zones=zones)
|
|
|
|
|
|
@app.route("/nat")
|
|
def nat_page():
|
|
zones = {}
|
|
for name in _safely(get_active_zones, {}):
|
|
zones[name] = _safely(lambda n=name: get_zone_info(n), {})
|
|
return render_template("nat.html", zones=zones)
|
|
|
|
|
|
@app.route("/dhcp")
|
|
def dhcp_page():
|
|
return render_template(
|
|
"dhcp.html",
|
|
config=_safely(dnsmasq_config, {}),
|
|
status=_safely(dnsmasq_status, {}),
|
|
leases=_safely(get_lease_table, []),
|
|
)
|
|
|
|
|
|
@app.route("/proxy")
|
|
def proxy_page():
|
|
return render_template(
|
|
"proxy.html", domains=_safely(get_domains, []), config=_safely(nginx_config, {})
|
|
)
|
|
|
|
|
|
@app.route("/certs")
|
|
def certs_page():
|
|
return render_template(
|
|
"certs.html", certs=_safely(list_certs, []), email=_safely(get_email, "")
|
|
)
|
|
|
|
|
|
@app.route("/wireguard")
|
|
def wireguard_page():
|
|
return render_template(
|
|
"wireguard.html", config=_safely(wg_config, {}), status=_safely(wg_status, {})
|
|
)
|
|
|
|
|
|
@app.route("/logs")
|
|
def logs_page():
|
|
return render_template("logs.html")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="127.0.0.1", port=9090)
|