Replace the Asterisk Admin iframe with a native Caddy path-proxy
No more cross-origin iframe: the Security Dashboard now reverse-proxies
the real Asterisk web admin natively at /asterisk-admin/ on its own
domain via Caddy's handle_path, instead of embedding a separate site in
a frame. One domain, one login wall, for both.
- services/asterisk.sh / services/asterisk-digital-ocean.sh: patch the
vendored web admin's one hardcoded absolute API path
(`const API_BASE = '/api'`, confirmed via the real vendored source to
be the only absolute-path reference anywhere in its HTML/JS - no other
hrefs, no login-page redirect, plain HTTP Basic Auth instead) so it
resolves correctly when mounted under a sub-path, via a new
WEBADMIN_BASE_PATH env var threaded through entrypoint.sh. Verified
against the real vendored file: patched output is
'/asterisk-admin/api' with the env var set, unchanged '/api' without
it. Skip each service's own dedicated admin Caddy domain when the
Security Dashboard is already installed, since it fronts the admin
instead.
- services/security-dashboard.sh: _secdash_configure_caddy now accepts
the admin's port and Asterisk's own directory/domain, path-routes
/asterisk-admin/* alongside the dashboard's own handle{} block, and
writes WEB_ADMIN_BASE_PATH into Asterisk's .env + restarts that
container once proxying is confirmed live. Defaults the dashboard's
own domain prompt to the droplet's DOMAIN_NAME when detected, since
Caddy's SIP-TLS cert sync already depends on serving that exact
domain. Removed the old CSP frame-ancestors patching and the iframe
itself; the nav is now a plain link, shown only once the proxy is
confirmed wired up.
- Fixed _secdash_remove_caddy_block's marker match to tolerate the
dashboard's reverse_proxy line now living one indent level deeper
(inside its own handle{} block) - verified against a synthetic
Caddyfile that it still finds and removes exactly the right block.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
This commit is contained in:
@@ -469,6 +469,75 @@ _asterisk_do_patch_messaging_vendor_files() {
|
||||
log_success "Vendor generator functions patched for internal SIP messaging."
|
||||
}
|
||||
|
||||
# ── Shared: sub-path-aware web admin (for native Caddy path-proxying) ──────
|
||||
# See services/asterisk.sh's own copy of this pair of functions for the full
|
||||
# rationale (verified against the real vendored file: the admin's only
|
||||
# absolute-path reference anywhere is `const API_BASE = '/api';`, no other
|
||||
# hrefs/redirects, plain HTTP Basic Auth instead of a login-page flow) —
|
||||
# identical here since both services vendor the exact same easy-asterisk
|
||||
# source, just under this service's own `_asterisk_do_` naming.
|
||||
_asterisk_do_patch_webadmin_base_path() {
|
||||
local EA_DIR="$1"
|
||||
local EASY1="$EA_DIR/easy-asterisk.sh"
|
||||
local EASY2
|
||||
EASY2="$(find "$EA_DIR" -maxdepth 1 -name 'easy-asterisk-v*.sh' | head -1)"
|
||||
[[ -z "$EASY2" ]] && EASY2="$EA_DIR/easy-asterisk-v0.10.0.sh"
|
||||
|
||||
local BASE_PATH_LINE="BASE_PATH = os.environ.get('WEBADMIN_BASE_PATH', '').rstrip('/')"
|
||||
local REPLACE_LINE
|
||||
REPLACE_LINE=$(cat <<'PYLINE'
|
||||
HTML_TEMPLATE = HTML_TEMPLATE.replace("const API_BASE = '/api';", "const API_BASE = '" + BASE_PATH + "/api';")
|
||||
PYLINE
|
||||
)
|
||||
|
||||
local f TMP_FILE
|
||||
for f in "$EASY1" "$EASY2"; do
|
||||
[[ -f "$f" ]] || continue
|
||||
grep -q "WEBADMIN_BASE_PATH" "$f" && continue # already patched
|
||||
|
||||
if ! grep -qF "PORT = int(os.environ.get('WEBADMIN_PORT', 8080))" "$f"; then
|
||||
log_warning "$(basename "$f"): WEBADMIN_PORT anchor not found — vendor template changed upstream."
|
||||
log_warning " Sub-path proxying for the web admin won't work correctly until this is patched by hand."
|
||||
continue
|
||||
fi
|
||||
if ! grep -qF "class WebAdminHandler(http.server.BaseHTTPRequestHandler):" "$f"; then
|
||||
log_warning "$(basename "$f"): WebAdminHandler anchor not found — vendor template changed upstream."
|
||||
log_warning " Sub-path proxying for the web admin won't work correctly until this is patched by hand."
|
||||
continue
|
||||
fi
|
||||
|
||||
TMP_FILE="$(mktemp)"
|
||||
awk -v base_line="$BASE_PATH_LINE" -v replace_line="$REPLACE_LINE" '
|
||||
index($0, "PORT = int(os.environ.get(") == 1 {
|
||||
print
|
||||
print base_line
|
||||
next
|
||||
}
|
||||
index($0, "class WebAdminHandler(http.server.BaseHTTPRequestHandler):") == 1 {
|
||||
print replace_line
|
||||
print ""
|
||||
}
|
||||
{ print }
|
||||
' "$f" > "$TMP_FILE" && mv "$TMP_FILE" "$f"
|
||||
done
|
||||
log_success "Vendor web admin script patched for sub-path proxying support."
|
||||
}
|
||||
|
||||
_asterisk_do_patch_webadmin_entrypoint_env() {
|
||||
local EA_DIR="$1"
|
||||
local ENTRYPOINT="$EA_DIR/docker/entrypoint.sh"
|
||||
[[ -f "$ENTRYPOINT" ]] || return 0
|
||||
grep -q "WEBADMIN_BASE_PATH=" "$ENTRYPOINT" && return 0 # already patched
|
||||
|
||||
if grep -qF 'WEBADMIN_AUTH_DISABLED="${WEB_ADMIN_AUTH_DISABLED:-false}" \' "$ENTRYPOINT"; then
|
||||
sed -i 's|WEBADMIN_AUTH_DISABLED="\${WEB_ADMIN_AUTH_DISABLED:-false}" \\|WEBADMIN_AUTH_DISABLED="${WEB_ADMIN_AUTH_DISABLED:-false}" \\\n WEBADMIN_BASE_PATH="${WEB_ADMIN_BASE_PATH:-}" \\|' "$ENTRYPOINT"
|
||||
log_success "Live entrypoint.sh patched to pass WEBADMIN_BASE_PATH through to the web admin."
|
||||
else
|
||||
log_warning "$(basename "$ENTRYPOINT"): WEBADMIN_AUTH_DISABLED anchor not found — vendor template changed upstream."
|
||||
log_warning " Sub-path proxying for the web admin won't work correctly until this is patched by hand."
|
||||
fi
|
||||
}
|
||||
|
||||
# Confirmed live (2026-07-23, via a real pstn-trunk.sh failure that hit this
|
||||
# same mechanism): the vendor-generator patch above only takes effect on a
|
||||
# FUTURE regeneration, and Easy Asterisk's own entrypoint only regenerates
|
||||
@@ -767,6 +836,9 @@ install_asterisk-digital-ocean() {
|
||||
_asterisk_do_write_messaging_dialplan "$EA_DIR/config/asterisk/messaging-dialplan.conf"
|
||||
_asterisk_do_ensure_live_messaging_include "$EA_DIR"
|
||||
_asterisk_do_migrate_existing_devices_message_context "$EA_DIR/config/asterisk/pjsip.conf"
|
||||
_asterisk_do_patch_webadmin_base_path "$EA_DIR"
|
||||
_asterisk_do_patch_webadmin_entrypoint_env "$EA_DIR"
|
||||
grep -q '^WEB_ADMIN_BASE_PATH=' .env || echo 'WEB_ADMIN_BASE_PATH=' >> .env
|
||||
ensure_docker_dir_ownership "$EA_DIR/config/asterisk"
|
||||
chmod 644 "$EA_DIR/config/asterisk/messaging-dialplan.conf"
|
||||
|
||||
@@ -843,6 +915,8 @@ install_asterisk-digital-ocean() {
|
||||
_asterisk_do_patch_messaging_vendor_files "$EA_DIR"
|
||||
_asterisk_do_write_messaging_dialplan "$EA_DIR/config/asterisk/messaging-dialplan.conf"
|
||||
_asterisk_do_ensure_live_messaging_include "$EA_DIR"
|
||||
_asterisk_do_patch_webadmin_base_path "$EA_DIR"
|
||||
_asterisk_do_patch_webadmin_entrypoint_env "$EA_DIR"
|
||||
ensure_docker_dir_ownership "$EA_DIR/config/asterisk"
|
||||
chmod 644 "$EA_DIR/config/asterisk/messaging-dialplan.conf"
|
||||
|
||||
@@ -907,6 +981,12 @@ install_asterisk-digital-ocean() {
|
||||
log_info "Port 8081 was already taken — web admin will use ${WEB_ADMIN_PORT_VAL} instead."
|
||||
fi
|
||||
|
||||
# If the Security Dashboard is already installed, it's going to front
|
||||
# this admin natively at /asterisk-admin/ (see services/security-dashboard.sh) —
|
||||
# pre-set the base path now so it works immediately, no update cycle needed.
|
||||
local WEB_ADMIN_BASE_PATH_VAL=""
|
||||
[[ -d "$DOCKER_DIR/security-dashboard" ]] && WEB_ADMIN_BASE_PATH_VAL="/asterisk-admin"
|
||||
|
||||
# ── .env ──────────────────────────────────────────────────────────────────
|
||||
cat > .env << ENV
|
||||
# ── Domain ────────────────────────────────────────────────────
|
||||
@@ -938,6 +1018,9 @@ VLAN_SUBNETS=
|
||||
# both firewall layers to match.
|
||||
WEB_ADMIN_PORT=${WEB_ADMIN_PORT_VAL}
|
||||
WEB_ADMIN_AUTH_DISABLED=false
|
||||
# Set to /asterisk-admin by the Security Dashboard when it fronts this admin
|
||||
# natively via Caddy path-proxying (no iframe) — leave empty otherwise.
|
||||
WEB_ADMIN_BASE_PATH=${WEB_ADMIN_BASE_PATH_VAL}
|
||||
ENV
|
||||
chmod 600 .env
|
||||
|
||||
@@ -959,6 +1042,27 @@ ENV
|
||||
log_info "No FQDN set — web admin stays on http://${PUBLIC_IP:-localhost}:${WEB_ADMIN_PORT_VAL} (nothing for Caddy to do)."
|
||||
elif [[ ! -d "$DOCKER_DIR/caddy" ]] && [[ -z "${CADDY_REMOTE_HOST:-}" ]]; then
|
||||
log_info "Caddy not installed — web admin stays on http://${PUBLIC_IP:-localhost}:${WEB_ADMIN_PORT_VAL}, SIP TLS stays self-signed."
|
||||
elif [[ -d "$DOCKER_DIR/security-dashboard" ]]; then
|
||||
# The dashboard owns fronting this admin instead — natively, at
|
||||
# https://<dashboard-domain>/asterisk-admin/, via Caddy path-proxying
|
||||
# (see services/security-dashboard.sh's _secdash_configure_caddy)
|
||||
# rather than a separate site block here. The SIP-TLS-cert-sync
|
||||
# requirement above only needs SOME active Caddy site block for
|
||||
# DOMAIN_NAME to exist — it doesn't require THIS service's own block
|
||||
# specifically — and the dashboard's own domain prompt defaults to
|
||||
# this exact DOMAIN_NAME when it detects this droplet, so the common
|
||||
# case still ends up with Caddy serving DOMAIN_NAME (satisfying SIP
|
||||
# TLS) with no separate admin domain needed at all.
|
||||
log_info "Security Dashboard detected — it fronts the Asterisk web admin natively"
|
||||
log_info "at https://<dashboard-domain>/asterisk-admin/ (no iframe, one URL for both)."
|
||||
log_info "Its own domain prompt defaults to this droplet's DOMAIN_NAME (${DOMAIN_NAME}),"
|
||||
log_info "so SIP TLS still gets a real cert as long as you accept that default."
|
||||
log_info "Re-run 'sudo ./setup.sh security-dashboard' (update mode) to reconfigure that."
|
||||
# Caddy (whichever domain the dashboard ends up using) reaches this
|
||||
# over the host's internal network either way — no need to also keep
|
||||
# the port open to the public internet, same as the local-Caddy case
|
||||
# just below.
|
||||
WEB_ADMIN_PUBLIC_ACCESS_NEEDED=false
|
||||
else
|
||||
local EXTRA_BLOCK=""
|
||||
if [ -d "$DOCKER_DIR/authelia" ]; then
|
||||
|
||||
+131
-8
@@ -433,6 +433,96 @@ _asterisk_patch_messaging_vendor_files() {
|
||||
log_success "Vendor generator functions patched for internal SIP messaging."
|
||||
}
|
||||
|
||||
# ── Shared: sub-path-aware web admin (for native Caddy path-proxying) ──────
|
||||
# The vendored web admin's own JS hardcodes `const API_BASE = '/api';` —
|
||||
# confirmed via grep against the actual vendored source: it's the ONLY
|
||||
# absolute-path reference anywhere in the admin's HTML/JS (no other hrefs,
|
||||
# no login-page redirect — it challenges with plain HTTP Basic Auth via a
|
||||
# 401/WWW-Authenticate response instead of a redirect flow). That one
|
||||
# hardcoded root path is what would break if this admin were ever reverse-
|
||||
# proxied on a sub-path (e.g. Caddy's `handle_path /asterisk-admin/*`)
|
||||
# instead of its own dedicated domain: the browser resolves each fetch()'s
|
||||
# absolute path against the current origin's ROOT, not the sub-path it was
|
||||
# actually served under, so every /api/... call 404s. This patches API_BASE
|
||||
# to prefix itself with a WEBADMIN_BASE_PATH env var (empty string = today's
|
||||
# behavior, completely unchanged) so a sub-path mount works correctly.
|
||||
# Verified against the real vendored file: patched output is
|
||||
# '/asterisk-admin/api' when the env var is set, and stays exactly '/api'
|
||||
# when it's unset — both confirmed by executing the patched module's
|
||||
# top-level code directly, not just eyeballing the diff.
|
||||
#
|
||||
# entrypoint.sh already regenerates this script UNCONDITIONALLY on every
|
||||
# container start (`easy-asterisk --write-web-admin-script`, no
|
||||
# `[[ ! -f ]]` guard unlike pjsip.conf/extensions.conf — confirmed in its
|
||||
# own source), so patching only the generator source here is sufficient;
|
||||
# no separate live-file patch is needed the way messaging needed one.
|
||||
_asterisk_patch_webadmin_base_path() {
|
||||
local EA_DIR="$1"
|
||||
local EASY1="$EA_DIR/easy-asterisk.sh"
|
||||
local EASY2
|
||||
EASY2="$(find "$EA_DIR" -maxdepth 1 -name 'easy-asterisk-v*.sh' | head -1)"
|
||||
[[ -z "$EASY2" ]] && EASY2="$EA_DIR/easy-asterisk-v0.10.0.sh"
|
||||
|
||||
local BASE_PATH_LINE="BASE_PATH = os.environ.get('WEBADMIN_BASE_PATH', '').rstrip('/')"
|
||||
local REPLACE_LINE
|
||||
REPLACE_LINE=$(cat <<'PYLINE'
|
||||
HTML_TEMPLATE = HTML_TEMPLATE.replace("const API_BASE = '/api';", "const API_BASE = '" + BASE_PATH + "/api';")
|
||||
PYLINE
|
||||
)
|
||||
|
||||
local f TMP_FILE
|
||||
for f in "$EASY1" "$EASY2"; do
|
||||
[[ -f "$f" ]] || continue
|
||||
grep -q "WEBADMIN_BASE_PATH" "$f" && continue # already patched
|
||||
|
||||
if ! grep -qF "PORT = int(os.environ.get('WEBADMIN_PORT', 8080))" "$f"; then
|
||||
log_warning "$(basename "$f"): WEBADMIN_PORT anchor not found — vendor template changed upstream."
|
||||
log_warning " Sub-path proxying for the web admin won't work correctly until this is patched by hand."
|
||||
continue
|
||||
fi
|
||||
if ! grep -qF "class WebAdminHandler(http.server.BaseHTTPRequestHandler):" "$f"; then
|
||||
log_warning "$(basename "$f"): WebAdminHandler anchor not found — vendor template changed upstream."
|
||||
log_warning " Sub-path proxying for the web admin won't work correctly until this is patched by hand."
|
||||
continue
|
||||
fi
|
||||
|
||||
TMP_FILE="$(mktemp)"
|
||||
awk -v base_line="$BASE_PATH_LINE" -v replace_line="$REPLACE_LINE" '
|
||||
index($0, "PORT = int(os.environ.get(") == 1 {
|
||||
print
|
||||
print base_line
|
||||
next
|
||||
}
|
||||
index($0, "class WebAdminHandler(http.server.BaseHTTPRequestHandler):") == 1 {
|
||||
print replace_line
|
||||
print ""
|
||||
}
|
||||
{ print }
|
||||
' "$f" > "$TMP_FILE" && mv "$TMP_FILE" "$f"
|
||||
done
|
||||
log_success "Vendor web admin script patched for sub-path proxying support."
|
||||
}
|
||||
|
||||
# Companion to the above: entrypoint.sh explicitly passes only WEBADMIN_PORT
|
||||
# and WEBADMIN_AUTH_DISABLED as env vars to the web admin script (see its own
|
||||
# "Start Web Admin in background" step) — WEBADMIN_BASE_PATH needs the same
|
||||
# explicit pass-through, or the container's own WEB_ADMIN_BASE_PATH (from
|
||||
# .env) never actually reaches the Python process reading it.
|
||||
_asterisk_patch_webadmin_entrypoint_env() {
|
||||
local EA_DIR="$1"
|
||||
local ENTRYPOINT="$EA_DIR/docker/entrypoint.sh"
|
||||
[[ -f "$ENTRYPOINT" ]] || return 0
|
||||
grep -q "WEBADMIN_BASE_PATH=" "$ENTRYPOINT" && return 0 # already patched
|
||||
|
||||
if grep -qF 'WEBADMIN_AUTH_DISABLED="${WEB_ADMIN_AUTH_DISABLED:-false}" \' "$ENTRYPOINT"; then
|
||||
sed -i 's|WEBADMIN_AUTH_DISABLED="\${WEB_ADMIN_AUTH_DISABLED:-false}" \\|WEBADMIN_AUTH_DISABLED="${WEB_ADMIN_AUTH_DISABLED:-false}" \\\n WEBADMIN_BASE_PATH="${WEB_ADMIN_BASE_PATH:-}" \\|' "$ENTRYPOINT"
|
||||
log_success "Live entrypoint.sh patched to pass WEBADMIN_BASE_PATH through to the web admin."
|
||||
else
|
||||
log_warning "$(basename "$ENTRYPOINT"): WEBADMIN_AUTH_DISABLED anchor not found — vendor template changed upstream."
|
||||
log_warning " Sub-path proxying for the web admin won't work correctly until this is patched by hand."
|
||||
fi
|
||||
}
|
||||
|
||||
# Confirmed live (2026-07-23, via a real pstn-trunk.sh failure that hit this
|
||||
# same mechanism): the vendor-generator patch above only takes effect on a
|
||||
# FUTURE regeneration, and Easy Asterisk's own entrypoint only regenerates
|
||||
@@ -726,6 +816,9 @@ install_asterisk() {
|
||||
_asterisk_write_messaging_dialplan "$EA_DIR/config/asterisk/messaging-dialplan.conf"
|
||||
_asterisk_ensure_live_messaging_include "$EA_DIR"
|
||||
_asterisk_migrate_existing_devices_message_context "$EA_DIR/config/asterisk/pjsip.conf"
|
||||
_asterisk_patch_webadmin_base_path "$EA_DIR"
|
||||
_asterisk_patch_webadmin_entrypoint_env "$EA_DIR"
|
||||
grep -q '^WEB_ADMIN_BASE_PATH=' .env || echo 'WEB_ADMIN_BASE_PATH=' >> .env
|
||||
ensure_docker_dir_ownership "$EA_DIR/config/asterisk"
|
||||
chmod 644 "$EA_DIR/config/asterisk/messaging-dialplan.conf"
|
||||
|
||||
@@ -771,6 +864,8 @@ install_asterisk() {
|
||||
_asterisk_refresh_vendor_files
|
||||
_asterisk_patch_messaging_vendor_files "$EA_DIR"
|
||||
_asterisk_write_messaging_dialplan "$EA_DIR/config/asterisk/messaging-dialplan.conf"
|
||||
_asterisk_patch_webadmin_base_path "$EA_DIR"
|
||||
_asterisk_patch_webadmin_entrypoint_env "$EA_DIR"
|
||||
ensure_docker_dir_ownership "$EA_DIR/config/asterisk"
|
||||
chmod 644 "$EA_DIR/config/asterisk/messaging-dialplan.conf"
|
||||
|
||||
@@ -841,6 +936,12 @@ install_asterisk() {
|
||||
log_info "Port 8081 was already taken — web admin will use ${WEB_ADMIN_PORT_VAL} instead."
|
||||
fi
|
||||
|
||||
# If the Security Dashboard is already installed, it's going to front
|
||||
# this admin natively at /asterisk-admin/ (see services/security-dashboard.sh) —
|
||||
# pre-set the base path now so it works immediately, no update cycle needed.
|
||||
local WEB_ADMIN_BASE_PATH_VAL=""
|
||||
[[ -d "$DOCKER_DIR/security-dashboard" ]] && WEB_ADMIN_BASE_PATH_VAL="/asterisk-admin"
|
||||
|
||||
# ── .env ──────────────────────────────────────────────────────────────────
|
||||
cat > .env << ENV
|
||||
# ── Domain ────────────────────────────────────────────────────
|
||||
@@ -871,6 +972,9 @@ VLAN_SUBNETS=${VLAN_SUBNETS_VAL}
|
||||
# any firewall rules to match.
|
||||
WEB_ADMIN_PORT=${WEB_ADMIN_PORT_VAL}
|
||||
WEB_ADMIN_AUTH_DISABLED=false
|
||||
# Set to /asterisk-admin by the Security Dashboard when it fronts this admin
|
||||
# natively via Caddy path-proxying (no iframe) — leave empty otherwise.
|
||||
WEB_ADMIN_BASE_PATH=${WEB_ADMIN_BASE_PATH_VAL}
|
||||
ENV
|
||||
chmod 600 .env
|
||||
|
||||
@@ -879,17 +983,36 @@ ENV
|
||||
# correctly: if a local Caddy ends up fronting the web admin, there's no
|
||||
# reason to also expose it on the LAN — Caddy already reaches it over
|
||||
# the host's internal network (host.docker.internal).
|
||||
#
|
||||
# If the Security Dashboard is already here, it owns fronting this admin
|
||||
# instead — natively, at https://<dashboard-domain>/asterisk-admin/, via
|
||||
# Caddy path-proxying (see services/security-dashboard.sh's
|
||||
# _secdash_configure_caddy) rather than a separate dedicated domain. Two
|
||||
# independent Caddy blocks both proxying the same port would just mean
|
||||
# two working URLs instead of one, defeating the point — skip this
|
||||
# service's own domain prompt entirely in that case. CADDY_SERVICE_MODE
|
||||
# is still "local" either way (Caddy reaches it over host.docker.internal
|
||||
# regardless of which Caddy block does the reaching), so the firewall
|
||||
# scoping below stays correct without changes.
|
||||
local EXTRA_BLOCK=""
|
||||
if [ -d "$DOCKER_DIR/authelia" ]; then
|
||||
local _use_auth=""
|
||||
prompt_yn "Protect Asterisk web admin with Authelia SSO? (y/n):" "y" _use_auth
|
||||
if [[ "$_use_auth" =~ ^[Yy]$ ]]; then
|
||||
EXTRA_BLOCK=" import authelia"
|
||||
# Disable built-in auth since Authelia handles it
|
||||
sed -i "s/^WEB_ADMIN_AUTH_DISABLED=.*/WEB_ADMIN_AUTH_DISABLED=true/" .env
|
||||
if [[ -d "$DOCKER_DIR/security-dashboard" ]]; then
|
||||
log_info "Security Dashboard detected — it fronts the Asterisk web admin natively"
|
||||
log_info "at https://<dashboard-domain>/asterisk-admin/ (no iframe, one URL for both)."
|
||||
log_info "Re-run 'sudo ./setup.sh security-dashboard' (update mode) to reconfigure that."
|
||||
CADDY_SERVICE_CONFIGURED=true
|
||||
CADDY_SERVICE_MODE=local
|
||||
else
|
||||
if [ -d "$DOCKER_DIR/authelia" ]; then
|
||||
local _use_auth=""
|
||||
prompt_yn "Protect Asterisk web admin with Authelia SSO? (y/n):" "y" _use_auth
|
||||
if [[ "$_use_auth" =~ ^[Yy]$ ]]; then
|
||||
EXTRA_BLOCK=" import authelia"
|
||||
# Disable built-in auth since Authelia handles it
|
||||
sed -i "s/^WEB_ADMIN_AUTH_DISABLED=.*/WEB_ADMIN_AUTH_DISABLED=true/" .env
|
||||
fi
|
||||
fi
|
||||
configure_caddy_for_service "Asterisk Web Admin" "${WEB_ADMIN_PORT_VAL}" "asterisk" "$EXTRA_BLOCK"
|
||||
fi
|
||||
configure_caddy_for_service "Asterisk Web Admin" "${WEB_ADMIN_PORT_VAL}" "asterisk" "$EXTRA_BLOCK"
|
||||
|
||||
# ── UFW firewall rules ────────────────────────────────────────────────────
|
||||
if command -v ufw &>/dev/null; then
|
||||
|
||||
+113
-113
@@ -83,11 +83,17 @@ install_security-dashboard() {
|
||||
local ASTERISK_LOG_DIR="${ASTERISK_EA_DIR:+$ASTERISK_EA_DIR/logs}"
|
||||
local ASTERISK_CONFIG_DIR="${ASTERISK_EA_DIR:+$ASTERISK_EA_DIR/config/asterisk}"
|
||||
|
||||
local ASTERISK_ADMIN_URL=""
|
||||
# Port to natively path-proxy at /asterisk-admin/ (see
|
||||
# _secdash_configure_caddy) — no separate domain, no iframe. EA_DOMAIN is
|
||||
# only used to default the dashboard's OWN domain prompt: on a droplet,
|
||||
# DOMAIN_NAME is already required to match Caddy's cert for SIP TLS (see
|
||||
# services/asterisk-digital-ocean.sh), so defaulting to the SAME domain
|
||||
# here is what makes "one URL" the path of least resistance instead of
|
||||
# something the user has to know to ask for.
|
||||
local ASTERISK_ADMIN_PORT="" ASTERISK_EA_DOMAIN=""
|
||||
if [ -n "$ASTERISK_EA_DIR" ] && [ -f "$ASTERISK_EA_DIR/.env" ]; then
|
||||
local _ea_domain
|
||||
_ea_domain="$(grep -E '^DOMAIN_NAME=' "$ASTERISK_EA_DIR/.env" | cut -d= -f2-)"
|
||||
[ -n "$_ea_domain" ] && ASTERISK_ADMIN_URL="https://${_ea_domain}"
|
||||
ASTERISK_ADMIN_PORT="$(grep -E '^WEB_ADMIN_PORT=' "$ASTERISK_EA_DIR/.env" | cut -d= -f2-)"
|
||||
ASTERISK_EA_DOMAIN="$(grep -E '^DOMAIN_NAME=' "$ASTERISK_EA_DIR/.env" | cut -d= -f2-)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
@@ -131,7 +137,15 @@ install_security-dashboard() {
|
||||
_secdash_write_app "$APP_DIR"
|
||||
_secdash_write_asn_helper "$APP_DIR"
|
||||
_secdash_write_sudoers "$SVC_USER"
|
||||
_secdash_write_systemd_unit "$APP_DIR" "$SVC_USER" "$DASHBOARD_PORT" "$ASTERISK_LOG_DIR" "$ASTERISK_CONFIG_DIR" "$ASTERISK_ADMIN_URL"
|
||||
|
||||
# Preserve whatever this box's ASTERISK_ADMIN_PROXIED already
|
||||
# is (we're not touching Caddy in this branch) instead of
|
||||
# silently defaulting it back to false on every plain update.
|
||||
local _CUR_ADMIN_PROXIED="false"
|
||||
if [ -f /etc/systemd/system/security-dashboard.service ]; then
|
||||
_CUR_ADMIN_PROXIED="$(grep -oP '(?<=ASTERISK_ADMIN_PROXIED=)\S+' /etc/systemd/system/security-dashboard.service 2>/dev/null || echo false)"
|
||||
fi
|
||||
_secdash_write_systemd_unit "$APP_DIR" "$SVC_USER" "$DASHBOARD_PORT" "$ASTERISK_LOG_DIR" "$ASTERISK_CONFIG_DIR" "$_CUR_ADMIN_PROXIED"
|
||||
systemctl restart security-dashboard 2>/dev/null \
|
||||
&& log_success "security-dashboard restarted" \
|
||||
|| log_warning "Restart failed — check: systemctl status security-dashboard"
|
||||
@@ -141,7 +155,10 @@ install_security-dashboard() {
|
||||
prompt_yn "Reconfigure this dashboard's Caddy protection (Authelia domain, or add/rotate an independent Basic Auth layer)? (y/n):" "n" _reconf
|
||||
if [[ "$_reconf" =~ ^[Yy]$ ]]; then
|
||||
_secdash_remove_caddy_block "$DASHBOARD_PORT"
|
||||
_secdash_configure_caddy "$DASHBOARD_PORT" "$ASTERISK_ADMIN_URL"
|
||||
SECDASH_ADMIN_PROXIED=false
|
||||
_secdash_configure_caddy "$DASHBOARD_PORT" "$ASTERISK_ADMIN_PORT" "$ASTERISK_EA_DIR" "$ASTERISK_EA_DOMAIN"
|
||||
_secdash_write_systemd_unit "$APP_DIR" "$SVC_USER" "$DASHBOARD_PORT" "$ASTERISK_LOG_DIR" "$ASTERISK_CONFIG_DIR" "$SECDASH_ADMIN_PROXIED"
|
||||
systemctl restart security-dashboard 2>/dev/null || true
|
||||
fi
|
||||
return 0
|
||||
;;
|
||||
@@ -167,7 +184,7 @@ install_security-dashboard() {
|
||||
_secdash_write_asn_helper "$APP_DIR"
|
||||
|
||||
_secdash_write_sudoers "$SVC_USER"
|
||||
_secdash_write_systemd_unit "$APP_DIR" "$SVC_USER" "$DASHBOARD_PORT" "$ASTERISK_LOG_DIR" "$ASTERISK_CONFIG_DIR" "$ASTERISK_ADMIN_URL"
|
||||
_secdash_write_systemd_unit "$APP_DIR" "$SVC_USER" "$DASHBOARD_PORT" "$ASTERISK_LOG_DIR" "$ASTERISK_CONFIG_DIR" "false"
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable security-dashboard >/dev/null 2>&1
|
||||
@@ -184,7 +201,12 @@ install_security-dashboard() {
|
||||
# _secdash_configure_caddy so "update" mode can also offer to reconfigure
|
||||
# it later (e.g. to add Basic Auth to an already-deployed dashboard)
|
||||
# without duplicating this logic — see that function for the rest.
|
||||
_secdash_configure_caddy "$DASHBOARD_PORT" "$ASTERISK_ADMIN_URL"
|
||||
SECDASH_ADMIN_PROXIED=false
|
||||
_secdash_configure_caddy "$DASHBOARD_PORT" "$ASTERISK_ADMIN_PORT" "$ASTERISK_EA_DIR" "$ASTERISK_EA_DOMAIN"
|
||||
if [[ "$SECDASH_ADMIN_PROXIED" == true ]]; then
|
||||
_secdash_write_systemd_unit "$APP_DIR" "$SVC_USER" "$DASHBOARD_PORT" "$ASTERISK_LOG_DIR" "$ASTERISK_CONFIG_DIR" "true"
|
||||
systemctl restart security-dashboard 2>/dev/null || true
|
||||
fi
|
||||
|
||||
write_readme "$APP_DIR" << README_MD
|
||||
# Security Dashboard
|
||||
@@ -206,20 +228,20 @@ CrowdSec, without ever showing a tab for something that isn't set up.
|
||||
- **Security Log** — parses \`$ASTERISK_LOG_DIR/full\` for SIP auth failures
|
||||
(wrong password, unknown extension, etc.) with timestamp/account/remote IP,
|
||||
sortable per column (click a header to sort, click again to reverse).
|
||||
- **Asterisk Admin** — an embedded, lazy-loaded iframe of the real Asterisk
|
||||
web admin (only fetched the first time you open the tab), plus an
|
||||
"open in a new tab" fallback link that's always there regardless. Its nav
|
||||
button only appears once an Asterisk install is detected. If a local Caddy
|
||||
install is found for both this dashboard and the Asterisk admin's own
|
||||
domain, install automatically patches the admin's Caddy site block from
|
||||
`X-Frame-Options` to a `Content-Security-Policy: frame-ancestors` entry
|
||||
naming only this dashboard's domain, so the browser actually allows the
|
||||
frame — every other site is still refused framing exactly as before. This
|
||||
is best-effort (it depends on matching the exact header line
|
||||
`services/asterisk-digital-ocean.sh` itself writes, and hasn't been
|
||||
confirmed against Authelia's own portal-framing behavior on a live
|
||||
install) — if the tab shows a blank frame, use the fallback link and check
|
||||
this service's own log output from install time for a manual one-line fix.
|
||||
- **Asterisk Admin** — a link to the real Asterisk web admin, proxied
|
||||
natively at \`/asterisk-admin/\` on this SAME domain — no iframe, no
|
||||
separate domain to log into. Wired up by \`_secdash_configure_caddy\`: Caddy
|
||||
path-routes that address straight to the admin container
|
||||
(\`host.docker.internal:<port>\`), and \`services/asterisk.sh\` /
|
||||
\`services/asterisk-digital-ocean.sh\` patch the vendored admin's one
|
||||
hardcoded absolute API path (\`const API_BASE = '/api'\`, confirmed via the
|
||||
vendored source to be the ONLY absolute-path reference anywhere in its
|
||||
HTML/JS) so it resolves correctly under that sub-path via a
|
||||
\`WEBADMIN_BASE_PATH\` env var. This link only appears once the proxy is
|
||||
actually wired up (not just because an Asterisk install exists). On a
|
||||
droplet, accepting this dashboard's domain prompt default (the droplet's
|
||||
own \`DOMAIN_NAME\`) is what keeps Caddy's SIP-TLS cert sync working too —
|
||||
entering a different domain there means SIP TLS falls back to self-signed.
|
||||
- **Extensions** — always available, independent of any PSTN trunk. A
|
||||
**Groups** card lets you name a set of extensions and bulk-enable/disable
|
||||
messaging for all of them at once — a management convenience only, not a
|
||||
@@ -335,7 +357,7 @@ _secdash_grant_asterisk_access() {
|
||||
# both layers (this AND the group access above) need to agree, or writes
|
||||
# fail even when Unix permissions alone would have allowed them.
|
||||
_secdash_write_systemd_unit() {
|
||||
local _app_dir="$1" _svc_user="$2" _port="$3" _log_dir="$4" _config_dir="$5" _admin_url="$6"
|
||||
local _app_dir="$1" _svc_user="$2" _port="$3" _log_dir="$4" _config_dir="$5" _admin_proxied="$6"
|
||||
local _read_only_paths="" _read_write_paths="/etc/crowdsec/scenarios"
|
||||
[ -n "$_log_dir" ] && _read_only_paths="$_log_dir"
|
||||
[ -n "$_config_dir" ] && _read_write_paths="$_read_write_paths $_config_dir"
|
||||
@@ -352,7 +374,7 @@ Group=$_svc_user
|
||||
Environment=DASHBOARD_PORT=$_port
|
||||
Environment=ASTERISK_LOG=${_log_dir:+$_log_dir/full}
|
||||
Environment=ASTERISK_CONFIG_DIR=$_config_dir
|
||||
Environment=ASTERISK_ADMIN_URL=$_admin_url
|
||||
Environment=ASTERISK_ADMIN_PROXIED=$_admin_proxied
|
||||
ExecStart=/usr/bin/python3 $_app_dir/app.py
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
@@ -398,7 +420,8 @@ SUDOERS
|
||||
# retroactively) using the exact same code path as a fresh install, instead
|
||||
# of hand-patching a live Caddyfile block in place.
|
||||
_secdash_configure_caddy() {
|
||||
local DASHBOARD_PORT="$1" ADMIN_URL="${2:-}"
|
||||
local DASHBOARD_PORT="$1" ADMIN_PORT="${2:-}" ASTERISK_EA_DIR="${3:-}" ASTERISK_EA_DOMAIN="${4:-}"
|
||||
SECDASH_ADMIN_PROXIED=false
|
||||
|
||||
echo ""
|
||||
if ! command -v docker &>/dev/null || ! docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^caddy$"; then
|
||||
@@ -406,8 +429,13 @@ _secdash_configure_caddy() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
local _default_domain=""
|
||||
if [ -n "${SITE_DOMAIN:-}" ] && [ "$SITE_DOMAIN" != "example.com" ]; then
|
||||
# Defaults to the Asterisk droplet's own DOMAIN_NAME when detected (see
|
||||
# services/asterisk-digital-ocean.sh — that domain is already required
|
||||
# there for Caddy's SIP-TLS cert sync), so accepting the default is what
|
||||
# makes "one URL for everything" the path of least resistance instead of
|
||||
# something you have to know to ask for.
|
||||
local _default_domain="$ASTERISK_EA_DOMAIN"
|
||||
if [ -z "$_default_domain" ] && [ -n "${SITE_DOMAIN:-}" ] && [ "$SITE_DOMAIN" != "example.com" ]; then
|
||||
_default_domain="security.${SITE_DOMAIN}"
|
||||
fi
|
||||
local SD_DOMAIN=""
|
||||
@@ -417,6 +445,11 @@ _secdash_configure_caddy() {
|
||||
log_warning "No domain entered — dashboard stays on http://localhost:$DASHBOARD_PORT only (not reachable from outside this box)."
|
||||
return 0
|
||||
fi
|
||||
if [ -n "$ADMIN_PORT" ] && [ -n "$ASTERISK_EA_DOMAIN" ] && [ "$SD_DOMAIN" != "$ASTERISK_EA_DOMAIN" ]; then
|
||||
log_warning "This differs from the Asterisk droplet's own DOMAIN_NAME (${ASTERISK_EA_DOMAIN})."
|
||||
log_warning "SIP TLS needs Caddy actively serving THAT exact domain — using a different one here"
|
||||
log_warning "means Caddy never gets a cert for it, and SIP TLS falls back to self-signed."
|
||||
fi
|
||||
|
||||
local EXTRA_BLOCK=""
|
||||
if [ -d "$DOCKER_DIR/authelia" ]; then
|
||||
@@ -491,6 +524,21 @@ _secdash_configure_caddy() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Path-proxies the real Asterisk web admin natively at /asterisk-admin/
|
||||
# on this SAME domain — no iframe, no separate domain for it. handle_path
|
||||
# strips the /asterisk-admin prefix before forwarding, so the admin
|
||||
# container sees requests exactly as if it were mounted at its own root
|
||||
# (same as today) — its own JS is still taught the real mount point via
|
||||
# WEBADMIN_BASE_PATH below, since its one absolute-path API reference
|
||||
# would otherwise resolve against this domain's true root instead.
|
||||
local ADMIN_HANDLE=""
|
||||
if [ -n "$ADMIN_PORT" ]; then
|
||||
ADMIN_HANDLE=" handle_path /asterisk-admin/* {
|
||||
reverse_proxy host.docker.internal:${ADMIN_PORT}
|
||||
}
|
||||
"
|
||||
fi
|
||||
|
||||
local CADDY_FILE="$DOCKER_DIR/caddy/Caddyfile"
|
||||
if [ -f "$CADDY_FILE" ] && ! grep -q "^${SD_DOMAIN} {" "$CADDY_FILE"; then
|
||||
cat >> "$CADDY_FILE" << CADDYBLOCK
|
||||
@@ -498,7 +546,9 @@ _secdash_configure_caddy() {
|
||||
# Security Dashboard
|
||||
${SD_DOMAIN} {
|
||||
${BASICAUTH_BLOCK}${EXTRA_BLOCK}
|
||||
reverse_proxy host.docker.internal:${DASHBOARD_PORT}
|
||||
${ADMIN_HANDLE} handle {
|
||||
reverse_proxy host.docker.internal:${DASHBOARD_PORT}
|
||||
}
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
@@ -517,6 +567,10 @@ CADDYBLOCK
|
||||
docker compose -f "$DOCKER_DIR/caddy/docker-compose.yml" restart caddy 2>/dev/null \
|
||||
&& log_success "Caddy restarted — dashboard at https://${SD_DOMAIN}" \
|
||||
|| log_warning "Restart Caddy manually: cd $DOCKER_DIR/caddy && docker compose restart"
|
||||
if [ -n "$ADMIN_PORT" ]; then
|
||||
log_success "Asterisk web admin proxied natively at https://${SD_DOMAIN}/asterisk-admin/ — no iframe, same domain as the dashboard."
|
||||
SECDASH_ADMIN_PROXIED=true
|
||||
fi
|
||||
elif [ -f "$CADDY_FILE" ]; then
|
||||
log_warning "$SD_DOMAIN already in Caddyfile — leaving the existing entry alone."
|
||||
fi
|
||||
@@ -530,64 +584,26 @@ CADDYBLOCK
|
||||
fi
|
||||
fi
|
||||
|
||||
_secdash_allow_asterisk_admin_iframe "$ADMIN_URL" "$SD_DOMAIN"
|
||||
}
|
||||
|
||||
# Best-effort: lets the dashboard's "Asterisk Admin" tab iframe-embed the
|
||||
# real Asterisk web admin, by swapping that domain's own Caddy site block
|
||||
# from X-Frame-Options to a CSP frame-ancestors entry naming ONLY this
|
||||
# dashboard's domain — every other site is still refused framing exactly as
|
||||
# before, this just relaxes it for the one origin that's supposed to embed
|
||||
# it. Best-effort because it depends on finding the exact
|
||||
# X-Frame-Options line services/asterisk-digital-ocean.sh itself generates,
|
||||
# inside a live Caddyfile it doesn't own — if that block was hand-edited
|
||||
# since, or doesn't exist yet (Asterisk installed after this dashboard, or
|
||||
# no local Caddy at all), this silently does nothing and the tab's "open in
|
||||
# a new tab" fallback link still works either way.
|
||||
_secdash_allow_asterisk_admin_iframe() {
|
||||
local ADMIN_URL="$1" SD_DOMAIN="$2"
|
||||
[ -n "$ADMIN_URL" ] || return 0
|
||||
[ -n "$SD_DOMAIN" ] || return 0
|
||||
command -v docker &>/dev/null || return 0
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^caddy$" || return 0
|
||||
|
||||
local ADMIN_DOMAIN="${ADMIN_URL#https://}"
|
||||
ADMIN_DOMAIN="${ADMIN_DOMAIN#http://}"
|
||||
local CADDY_FILE="$DOCKER_DIR/caddy/Caddyfile"
|
||||
[ -f "$CADDY_FILE" ] || return 0
|
||||
grep -q "^${ADMIN_DOMAIN} {" "$CADDY_FILE" || return 0
|
||||
|
||||
if grep -qF "frame-ancestors 'self' https://${SD_DOMAIN};" "$CADDY_FILE"; then
|
||||
return 0 # already patched for this exact dashboard domain
|
||||
fi
|
||||
|
||||
local CSP_LINE=" Content-Security-Policy \"frame-ancestors 'self' https://${SD_DOMAIN};\""
|
||||
local TMP_FILE
|
||||
TMP_FILE="$(mktemp)"
|
||||
awk -v domain="${ADMIN_DOMAIN} {" -v csp="$CSP_LINE" '
|
||||
BEGIN { in_block = 0; patched = 0 }
|
||||
index($0, domain) == 1 { in_block = 1 }
|
||||
in_block && !patched && /X-Frame-Options/ { print csp; patched = 1; next }
|
||||
{ print }
|
||||
in_block && /^}/ { in_block = 0 }
|
||||
' "$CADDY_FILE" > "$TMP_FILE"
|
||||
|
||||
if grep -qF "frame-ancestors 'self' https://${SD_DOMAIN};" "$TMP_FILE"; then
|
||||
cp "$CADDY_FILE" "$CADDY_FILE.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
mv "$TMP_FILE" "$CADDY_FILE"
|
||||
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
|
||||
if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null || docker restart caddy &>/dev/null; then
|
||||
log_success "Asterisk web admin (${ADMIN_DOMAIN}) now allows embedding from https://${SD_DOMAIN} — the dashboard's Asterisk Admin tab should load it."
|
||||
# Tell the admin's own vendor-patched web admin script it's now mounted
|
||||
# under /asterisk-admin instead of the root, and restart that container
|
||||
# so the change actually takes effect (entrypoint.sh regenerates the
|
||||
# script unconditionally on every start — see
|
||||
# _asterisk_patch_webadmin_base_path in services/asterisk.sh /
|
||||
# services/asterisk-digital-ocean.sh for the full rationale).
|
||||
if [ "$SECDASH_ADMIN_PROXIED" = true ] && [ -n "$ASTERISK_EA_DIR" ] && [ -f "$ASTERISK_EA_DIR/.env" ]; then
|
||||
if grep -q '^WEB_ADMIN_BASE_PATH=' "$ASTERISK_EA_DIR/.env"; then
|
||||
sed -i 's#^WEB_ADMIN_BASE_PATH=.*#WEB_ADMIN_BASE_PATH=/asterisk-admin#' "$ASTERISK_EA_DIR/.env"
|
||||
else
|
||||
log_warning "Caddyfile patched, but reload/restart failed — check: docker logs caddy"
|
||||
echo 'WEB_ADMIN_BASE_PATH=/asterisk-admin' >> "$ASTERISK_EA_DIR/.env"
|
||||
fi
|
||||
local _ea_container="easy-asterisk"
|
||||
[[ "$ASTERISK_EA_DIR" == *asterisk-digital-ocean ]] && _ea_container="easy-asterisk-do"
|
||||
if docker restart "$_ea_container" &>/dev/null; then
|
||||
log_success "Restarted $_ea_container to pick up the new web admin mount point."
|
||||
else
|
||||
log_warning "Couldn't restart $_ea_container automatically — restart it yourself so the web admin picks up WEB_ADMIN_BASE_PATH:"
|
||||
log_warning " docker restart $_ea_container"
|
||||
fi
|
||||
else
|
||||
rm -f "$TMP_FILE"
|
||||
log_warning "Couldn't find an X-Frame-Options line in ${ADMIN_DOMAIN}'s Caddy block to patch —"
|
||||
log_warning "the dashboard's Asterisk Admin tab will show a blank frame. Add this line yourself"
|
||||
log_warning "inside that domain's 'header { }' block in $CADDY_FILE, replacing X-Frame-Options:"
|
||||
log_warning " Content-Security-Policy \"frame-ancestors 'self' https://${SD_DOMAIN};\""
|
||||
log_warning "then: docker exec caddy caddy reload --config /etc/caddy/Caddyfile"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -603,9 +619,12 @@ _secdash_remove_caddy_block() {
|
||||
local caddy_file="$DOCKER_DIR/caddy/Caddyfile"
|
||||
[ -f "$caddy_file" ] || return 0
|
||||
|
||||
local marker=" reverse_proxy host.docker.internal:${port}"
|
||||
# Any amount of leading whitespace — this line now lives inside a
|
||||
# "handle { }" sub-block (one level deeper than before the native
|
||||
# Asterisk-admin path-proxy was added), not at the old fixed 4-space
|
||||
# indent directly under the domain block.
|
||||
local marker_line domain_line end_line
|
||||
marker_line="$(grep -nF "$marker" "$caddy_file" | head -1 | cut -d: -f1)"
|
||||
marker_line="$(grep -nE "^[[:space:]]*reverse_proxy host\.docker\.internal:${port}\$" "$caddy_file" | head -1 | cut -d: -f1)"
|
||||
if [ -z "$marker_line" ]; then
|
||||
return 0 # nothing deployed yet — fine, the fresh flow will just append
|
||||
fi
|
||||
@@ -728,7 +747,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
PORT = int(os.environ.get("DASHBOARD_PORT", "8092"))
|
||||
ASTERISK_LOG = os.environ.get("ASTERISK_LOG", "")
|
||||
ASTERISK_ADMIN_URL = os.environ.get("ASTERISK_ADMIN_URL", "")
|
||||
ASTERISK_ADMIN_PROXIED = os.environ.get("ASTERISK_ADMIN_PROXIED", "false").lower() == "true"
|
||||
ASTERISK_CONFIG_DIR = os.environ.get("ASTERISK_CONFIG_DIR", "")
|
||||
ASN_SCENARIO_FILES = [
|
||||
"/etc/crowdsec/scenarios/local-asterisk_bf.yaml",
|
||||
@@ -1512,8 +1531,9 @@ INDEX_HTML = """<!doctype html>
|
||||
body { font-family: system-ui, sans-serif; margin: 0; background: #0f1115; color: #e6e6e6; }
|
||||
header { padding: 1rem 1.5rem; background: #171a21; border-bottom: 1px solid #2a2e38; display: flex; align-items: center; gap: 1rem; }
|
||||
header h1 { font-size: 1.1rem; margin: 0; flex: 1; }
|
||||
nav button { background: none; border: none; color: #9aa4b2; padding: 0.6rem 1rem; cursor: pointer; font-size: 0.95rem; border-bottom: 2px solid transparent; }
|
||||
nav button, nav a.nav-link-btn { background: none; border: none; color: #9aa4b2; padding: 0.6rem 1rem; cursor: pointer; font-size: 0.95rem; border-bottom: 2px solid transparent; text-decoration: none; display: inline-block; }
|
||||
nav button.active { color: #fff; border-bottom-color: #4f8cff; }
|
||||
nav a.nav-link-btn:hover { color: #e6e6e6; }
|
||||
main { padding: 1.5rem; max-width: 1100px; margin: 0 auto; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
||||
th, td { text-align: left; padding: 0.5rem 0.6rem; border-bottom: 1px solid #23262f; }
|
||||
@@ -1541,7 +1561,7 @@ INDEX_HTML = """<!doctype html>
|
||||
<h1>Security Dashboard</h1>
|
||||
<nav>
|
||||
<button class="tab-btn active" data-tab="security">Security Log</button>
|
||||
<button class="tab-btn" id="asterisk-tab-btn" data-tab="asterisk" style="display:none">Asterisk Admin</button>
|
||||
<a class="nav-link-btn" id="asterisk-admin-link" href="/asterisk-admin/" target="_blank" style="display:none">Asterisk Admin ↗</a>
|
||||
<button class="tab-btn" data-tab="extensions">Extensions</button>
|
||||
<button class="tab-btn" id="pstn-tab-btn" data-tab="pstn" style="display:none">PSTN Trunk</button>
|
||||
<button class="tab-btn" id="crowdsec-tab-btn" data-tab="crowdsec" style="display:none">CrowdSec</button>
|
||||
@@ -1662,22 +1682,11 @@ INDEX_HTML = """<!doctype html>
|
||||
<div id="pd-msg" class="muted" style="margin-top:0.5rem"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-asterisk" style="display:none">
|
||||
<div class="card">
|
||||
<p class="muted">
|
||||
Embedded — not a copy, this is the real Asterisk web admin loaded live in a frame.
|
||||
If it logs you in separately (its own Authelia domain, or Basic Auth), that's expected —
|
||||
it's still a genuinely separate site under the hood.
|
||||
<a id="admin-link-fallback" href="#" target="_blank">Open in a new tab instead ↗</a>
|
||||
</p>
|
||||
<iframe id="admin-iframe" style="width:100%;height:80vh;border:1px solid #2a2e38;border-radius:8px;background:#0f1115"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
function esc(s) { return (s || "").replace(/[&<>"]/g, c => ({"&":"&","<":"<",">":">",'"':"""}[c])); }
|
||||
|
||||
const TABS = ["security", "asterisk", "extensions", "pstn", "crowdsec"];
|
||||
const TABS = ["security", "extensions", "pstn", "crowdsec"];
|
||||
document.querySelectorAll(".tab-btn").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
|
||||
@@ -1685,13 +1694,6 @@ document.querySelectorAll(".tab-btn").forEach(btn => {
|
||||
TABS.forEach(t => { document.getElementById("tab-" + t).style.display = btn.dataset.tab === t ? "" : "none"; });
|
||||
if (btn.dataset.tab === "extensions") { loadMessaging(); loadGroups(); }
|
||||
if (btn.dataset.tab === "pstn") { loadPstnLimits(); loadPstnPermissions(); loadPersonalDids(); }
|
||||
if (btn.dataset.tab === "asterisk") {
|
||||
// Lazy-loaded — only fetched the first time this tab is opened, not
|
||||
// on every dashboard page load (avoids an extra login prompt/request
|
||||
// to a separate site for people who never open this tab).
|
||||
const frame = document.getElementById("admin-iframe");
|
||||
if (!frame.src && adminUrl) frame.src = adminUrl;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2201,10 +2203,8 @@ async function removePersonalDid(did) {
|
||||
loadPersonalDids();
|
||||
}
|
||||
|
||||
const adminUrl = "__ASTERISK_ADMIN_URL__";
|
||||
if (adminUrl) {
|
||||
document.getElementById("asterisk-tab-btn").style.display = "";
|
||||
document.getElementById("admin-link-fallback").href = adminUrl;
|
||||
if ("__ASTERISK_ADMIN_PROXIED__") {
|
||||
document.getElementById("asterisk-admin-link").style.display = "";
|
||||
}
|
||||
|
||||
loadSecurity();
|
||||
@@ -2238,7 +2238,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == "/" or self.path == "":
|
||||
html = INDEX_HTML.replace("__ASTERISK_ADMIN_URL__", ASTERISK_ADMIN_URL)
|
||||
html = INDEX_HTML.replace("__ASTERISK_ADMIN_PROXIED__", "true" if ASTERISK_ADMIN_PROXIED else "")
|
||||
self._html(html)
|
||||
elif self.path == "/api/security-events":
|
||||
self._json(parse_security_log())
|
||||
|
||||
Reference in New Issue
Block a user