Merge pull request #211 from outis1one/claude/sip-voip-integration-atsins
Claude/sip voip integration atsins
This commit is contained in:
@@ -412,32 +412,34 @@ generator output. Fixed by quoting every value in that heredoc.
|
||||
`full`-tier extensions can use it regardless of which countries are
|
||||
allowed.
|
||||
11. Internal SIP `MESSAGE` (native Asterisk texting, no carrier SMS/cost) —
|
||||
**partially done**. The permission layer is real and live-editable: a
|
||||
`messaging=yes` flag per extension in `pstn-permissions.conf`,
|
||||
independent of the PSTN calling tiers (an extension can be
|
||||
internal-tier for calling and still messaging-enabled, or vice versa),
|
||||
prompted at install time AND now a checkbox right in the Security
|
||||
Dashboard's PSTN Trunk permissions table (alongside tier/approved-
|
||||
numbers) — no need to re-run the CLI installer just to change who can
|
||||
message. Confirmed it correctly survives tier changes and personal-DID
|
||||
assignment/removal on the same extension (this is what surfaced the
|
||||
tier=internal section-wipe bug fixed above). **Not done**: the actual
|
||||
dialplan wiring that would make Asterisk *enforce* this flag on
|
||||
inbound `MESSAGE` requests — this flag currently does nothing at the
|
||||
Asterisk level yet, it's groundwork.
|
||||
Reasoned through but deliberately not shipped: Easy Asterisk dispatches
|
||||
messages through the same `[intercom]` context calls use (no
|
||||
`message_context` override), and whether a hand-written pattern there
|
||||
would take precedence over — or conflict with — Easy Asterisk's own
|
||||
generated per-device dial patterns in that same context isn't something
|
||||
that can be safely determined without a live install to test against.
|
||||
Shipping a guessed pattern risked either silently not working or, worse,
|
||||
interfering with call-routing precedence for the same extensions.
|
||||
Treat this the same way as the VoIP.ms live-account verification in
|
||||
item 6 above: a real gap, flagged rather than papered over, not a
|
||||
hypothetical. Next step for whoever picks this up: verify message
|
||||
routing behavior against a live Easy Asterisk container, then wire the
|
||||
dialplan gate using the existing flag.
|
||||
**done**. The permission layer: a `messaging=yes` flag per extension in
|
||||
`pstn-permissions.conf`, independent of the PSTN calling tiers (an
|
||||
extension can be internal-tier for calling and still messaging-enabled,
|
||||
or vice versa), prompted at install time AND a checkbox right in the
|
||||
Security Dashboard's PSTN Trunk permissions table (alongside
|
||||
tier/approved-numbers) — no need to re-run the CLI installer just to
|
||||
change who can message. Confirmed it correctly survives tier changes and
|
||||
personal-DID assignment/removal on the same extension (this is what
|
||||
surfaced the tier=internal section-wipe bug fixed above).
|
||||
The dialplan gap flagged here previously is now closed: a real install's
|
||||
`pjsip.conf`/`extensions.conf` were pulled (2026-07-23) and confirmed
|
||||
every endpoint sets `context=intercom` with `message_context` blank
|
||||
(falls back to `context`), and `[intercom]` gets one exact-match
|
||||
`exten => <ext>,1,...` per device, freshly regenerated by Easy
|
||||
Asterisk's own `rebuild_dialplan()` on every dialplan rebuild — exactly
|
||||
the collision this doc worried about. Solved by NOT sharing
|
||||
`[intercom]`: `services/asterisk-digital-ocean.sh` now explicitly sets
|
||||
`message_context=sip-messaging` on every endpoint (patched into both of
|
||||
Easy Asterisk's device-creation code paths — the CLI menu's bash
|
||||
heredoc and the web admin's Python `add_device()` — so new devices pick
|
||||
it up automatically, plus a one-time migration for devices that already
|
||||
existed) and routes messages to a dedicated `[sip-messaging]` context in
|
||||
`messaging-dialplan.conf`, gated on the sender's `messaging` flag via
|
||||
`AST_CONFIG()`. Zero overlap with `[intercom]`'s own call routing.
|
||||
One piece still flagged rather than papered over: the `MESSAGE(from)`
|
||||
sender-extraction (`CUT()`-based, written to tolerate a display-name
|
||||
prefix) hasn't been confirmed against real MESSAGE traffic yet — fails
|
||||
closed (denies) if it ever parses wrong, but worth a live test.
|
||||
12. Anveo Direct's real-time \$0-balance blocking, confirmed — support reply
|
||||
(MFonk, 7/22/2026): "all calls (incoming and outgoing) will be blocked"
|
||||
at \$0, in real time, not just via a recurring-fee grace period. This
|
||||
|
||||
@@ -293,6 +293,334 @@ $_ea_dir/logs/full {
|
||||
LOGROTATE
|
||||
}
|
||||
|
||||
# ── Shared: extension presence (online/offline) ntfy alerts ────────────────
|
||||
# Polls PJSIP registration state and alerts only on a CHANGE from the last
|
||||
# check (never on every poll) — same periodic-check shape as pstn-trunk.sh's
|
||||
# usage-alert script, but purely informational, so a looser 2-minute
|
||||
# interval is fine here (nothing enforces/blocks anything off the back of
|
||||
# this one). UNVERIFIED: the `pjsip show contacts` column layout below is
|
||||
# parsed defensively (grep for the Avail/Unavail keyword rather than a fixed
|
||||
# column position) specifically because it hasn't been confirmed against a
|
||||
# live install's actual output yet — run
|
||||
# `docker exec easy-asterisk-do asterisk -rx "pjsip show contacts"` yourself
|
||||
# after enabling this to confirm extensions/status actually show up as
|
||||
# expected, same as any other not-yet-live-tested piece in this project.
|
||||
_asterisk_do_write_presence_alert_script() {
|
||||
local FILE="$1" CONTAINER_NAME="$2" NTFY_URL="$3" STATE_FILE="$4"
|
||||
cat > "$FILE" << 'SCRIPT'
|
||||
#!/bin/bash
|
||||
# Auto-generated by services/asterisk-digital-ocean.sh — rerun the installer's
|
||||
# presence-alert step to change settings instead of editing this directly.
|
||||
CONTAINER_NAME="__PRESENCE_CONTAINER__"
|
||||
NTFY_URL="__PRESENCE_NTFY_URL__"
|
||||
STATE_FILE="__PRESENCE_STATE_FILE__"
|
||||
|
||||
[[ -z "$NTFY_URL" ]] && exit 0
|
||||
|
||||
send_ntfy() {
|
||||
curl -m 5 -s -d "$1" "$NTFY_URL" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
CURRENT="$(docker exec "$CONTAINER_NAME" asterisk -rx "pjsip show contacts" 2>/dev/null | grep '^ Contact:' | while read -r _ aor rest; do
|
||||
ext="${aor%%/*}"
|
||||
status="Unknown"
|
||||
case "$rest" in
|
||||
*Unavail*) status="Unavail" ;;
|
||||
*Avail*) status="Avail" ;;
|
||||
esac
|
||||
echo "${ext}:${status}"
|
||||
done)"
|
||||
|
||||
[[ -z "$CURRENT" ]] && exit 0
|
||||
|
||||
touch "$STATE_FILE"
|
||||
declare -A OLD_STATE
|
||||
while IFS=: read -r ext status; do
|
||||
[[ -n "$ext" ]] && OLD_STATE["$ext"]="$status"
|
||||
done < "$STATE_FILE"
|
||||
|
||||
: > "${STATE_FILE}.new"
|
||||
while IFS=: read -r ext status; do
|
||||
[[ -z "$ext" ]] && continue
|
||||
echo "${ext}:${status}" >> "${STATE_FILE}.new"
|
||||
old="${OLD_STATE[$ext]:-}"
|
||||
if [[ -n "$old" && "$old" != "$status" && "$status" != "Unknown" ]]; then
|
||||
if [[ "$status" == "Avail" ]]; then
|
||||
send_ntfy "Extension $ext is back online."
|
||||
elif [[ "$old" == "Avail" ]]; then
|
||||
send_ntfy "Extension $ext went offline."
|
||||
fi
|
||||
fi
|
||||
done <<< "$CURRENT"
|
||||
mv "${STATE_FILE}.new" "$STATE_FILE"
|
||||
SCRIPT
|
||||
sed -i "s#__PRESENCE_CONTAINER__#${CONTAINER_NAME}#g; s#__PRESENCE_NTFY_URL__#${NTFY_URL}#g; s#__PRESENCE_STATE_FILE__#${STATE_FILE}#g" "$FILE"
|
||||
chmod 755 "$FILE"
|
||||
}
|
||||
|
||||
_asterisk_do_install_presence_timer() {
|
||||
local EA_DIR="$1"
|
||||
mkdir -p "$EA_DIR/logs"
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1 && [[ -d /run/systemd/system ]]; then
|
||||
cat > /etc/systemd/system/asterisk-presence-alert.service << SVCEOF
|
||||
[Unit]
|
||||
Description=Asterisk extension presence (online/offline) check
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/bash $EA_DIR/asterisk-presence-alert.sh
|
||||
StandardOutput=append:$EA_DIR/logs/asterisk-presence-alert.log
|
||||
StandardError=append:$EA_DIR/logs/asterisk-presence-alert.log
|
||||
SVCEOF
|
||||
|
||||
cat > /etc/systemd/system/asterisk-presence-alert.timer << SVCEOF
|
||||
[Unit]
|
||||
Description=Run the Asterisk presence check every 2 minutes
|
||||
|
||||
[Timer]
|
||||
OnBootSec=2min
|
||||
OnUnitActiveSec=2min
|
||||
AccuracySec=10s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
SVCEOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now asterisk-presence-alert.timer
|
||||
log_success "Presence check installed (systemd timer, every 2 minutes)."
|
||||
elif command -v cron >/dev/null 2>&1 || [[ -d /etc/cron.d ]]; then
|
||||
cat > /etc/cron.d/asterisk-presence-alert << CRON
|
||||
*/2 * * * * root /bin/bash $EA_DIR/asterisk-presence-alert.sh >> $EA_DIR/logs/asterisk-presence-alert.log 2>&1
|
||||
CRON
|
||||
log_success "Presence check installed (cron.d fallback — systemd not detected)."
|
||||
else
|
||||
log_warning "Neither systemd nor cron available — run $EA_DIR/asterisk-presence-alert.sh manually/periodically."
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Shared: internal SIP MESSAGE routing/enforcement ────────────────────────
|
||||
# Confirmed live against a real install's pjsip.conf/extensions.conf
|
||||
# (2026-07-23): every endpoint sets context=intercom and leaves
|
||||
# message_context blank, so PJSIP messaging falls back to context=intercom —
|
||||
# and [intercom] already owns an exact-match `exten => <ext>,1,...` per
|
||||
# device, freshly regenerated by the vendor's own rebuild_dialplan() on
|
||||
# every dialplan rebuild. A competing priority-1 declaration for the same
|
||||
# extension number in a #include'd file would race that (Asterisk doesn't
|
||||
# merge two independent priority-1 declarations for the same context+exten —
|
||||
# one silently wins) and risks breaking normal internal calling entirely.
|
||||
# So this uses its own dedicated [sip-messaging] context instead, reached by
|
||||
# explicitly setting message_context=sip-messaging on every endpoint, so
|
||||
# there is never any overlap with [intercom]'s own per-device call routing.
|
||||
#
|
||||
# The vendor's device-creation code has exactly two independent code paths
|
||||
# that write a fresh endpoint block (confirmed via grep — both contain the
|
||||
# literal line "context=intercom" exactly once): the CLI menu's bash heredoc,
|
||||
# and the web admin's Python add_device(). Patching the vendor's own
|
||||
# generator source (same technique as _pstn_patch_vendor_files) makes every
|
||||
# device added FROM NOW ON pick this up automatically, in either path.
|
||||
# Devices that already existed before this was installed need one one-time
|
||||
# migration pass over the live pjsip.conf (below) since they were written
|
||||
# before the patch existed.
|
||||
_asterisk_do_patch_messaging_vendor_files() {
|
||||
local EA_DIR="$1"
|
||||
local ENTRYPOINT="$EA_DIR/docker/entrypoint.sh"
|
||||
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 f
|
||||
|
||||
for f in "$EASY1" "$EASY2"; do
|
||||
[[ -f "$f" ]] || { log_error "$f not found — is the base Asterisk install fully set up?"; return 1; }
|
||||
done
|
||||
|
||||
# Device-creation templates: both occurrences of "context=intercom" in
|
||||
# these two files (identical vendor source, copied twice) are the CLI
|
||||
# and web-admin device-creation code paths — a single anchor on the bare
|
||||
# line patches both in one pass.
|
||||
for f in "$EASY1" "$EASY2"; do
|
||||
if ! grep -q '^message_context=sip-messaging$' "$f"; then
|
||||
if grep -q '^context=intercom$' "$f"; then
|
||||
sed -i '/^context=intercom$/a message_context=sip-messaging' "$f"
|
||||
else
|
||||
log_warning "$(basename "$f"): 'context=intercom' anchor not found — vendor template changed upstream."
|
||||
log_warning " Add 'message_context=sip-messaging' manually after every 'context=intercom' line in this file's device-creation code."
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# extensions.conf: same [intercom] anchor _pstn_patch_vendor_files uses,
|
||||
# a SEPARATE #include so this coexists whether or not pstn-trunk is
|
||||
# installed — messaging is independent of the PSTN trunk entirely.
|
||||
for f in "$ENTRYPOINT" "$EASY1" "$EASY2"; do
|
||||
[[ -f "$f" ]] || continue
|
||||
if ! grep -q 'messaging-dialplan.conf' "$f"; then
|
||||
if grep -q '^\[intercom\]$' "$f"; then
|
||||
sed -i '/^\[intercom\]$/a #include messaging-dialplan.conf' "$f"
|
||||
else
|
||||
log_warning "$(basename "$f"): '[intercom]' anchor not found — vendor template changed upstream."
|
||||
log_warning " Add '#include messaging-dialplan.conf' manually after [intercom] in this file's extensions.conf heredoc."
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
log_success "Vendor generator functions patched for internal SIP messaging."
|
||||
}
|
||||
|
||||
# One-time migration for devices that already existed before the patch above
|
||||
# — new devices pick up message_context=sip-messaging automatically from now
|
||||
# on, but anything already in pjsip.conf was written before that existed.
|
||||
# Idempotent: buffers the file and only inserts where the very next line
|
||||
# isn't already the exact value, so reruns (every "update") never duplicate it.
|
||||
_asterisk_do_migrate_existing_devices_message_context() {
|
||||
local PJSIP_FILE="$1"
|
||||
[[ -f "$PJSIP_FILE" ]] || return 0
|
||||
grep -q '^context=intercom$' "$PJSIP_FILE" || return 0
|
||||
|
||||
local TMP_FILE
|
||||
TMP_FILE="$(mktemp)"
|
||||
awk '
|
||||
{ lines[NR] = $0 }
|
||||
END {
|
||||
for (i = 1; i <= NR; i++) {
|
||||
print lines[i]
|
||||
if (lines[i] == "context=intercom" && lines[i+1] != "message_context=sip-messaging") {
|
||||
print "message_context=sip-messaging"
|
||||
}
|
||||
}
|
||||
}
|
||||
' "$PJSIP_FILE" > "$TMP_FILE"
|
||||
|
||||
if ! diff -q "$PJSIP_FILE" "$TMP_FILE" >/dev/null 2>&1; then
|
||||
cp "$PJSIP_FILE" "$PJSIP_FILE.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
mv "$TMP_FILE" "$PJSIP_FILE"
|
||||
chown asterisk:asterisk "$PJSIP_FILE" 2>/dev/null || true
|
||||
log_success "Existing devices migrated to message_context=sip-messaging (backup saved alongside pjsip.conf)."
|
||||
else
|
||||
rm -f "$TMP_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# The actual enforcement — gated on the SENDER's own "messaging" flag in
|
||||
# pstn-permissions.conf (the exact file/flag the Security Dashboard's
|
||||
# "Internal SIP messaging" checkbox writes, independent of whether the PSTN
|
||||
# trunk is installed), read live via AST_CONFIG() on every message, same
|
||||
# mechanism pstn-trunk.sh's own dialplan already relies on for permission
|
||||
# tiers — no restart needed to take effect. Off by default: an extension
|
||||
# with no entry, or messaging=no, is denied. UNVERIFIED: MESSAGE(from)'s
|
||||
# exact format hasn't been confirmed on a live install — the CUT()-based
|
||||
# extraction below is written to tolerate a display name (e.g. this
|
||||
# project's "name0" <999> callerid format) but if it ever fails to parse,
|
||||
# FROM_EXT ends up empty/wrong and the AST_CONFIG() lookup simply finds no
|
||||
# match, which denies by default (same fail-closed behavior as an
|
||||
# unlisted extension) rather than silently allowing anything through.
|
||||
_asterisk_do_write_messaging_dialplan() {
|
||||
local FILE="$1"
|
||||
cat > "$FILE" << 'EOF'
|
||||
; Internal SIP MESSAGE routing/enforcement — services/asterisk-digital-ocean.sh.
|
||||
; Regenerated on every install/update; edit there, not here directly.
|
||||
;
|
||||
; Reached via each endpoint's message_context=sip-messaging (patched into
|
||||
; Easy Asterisk's own device-creation code — see
|
||||
; _asterisk_do_patch_messaging_vendor_files) instead of falling back to
|
||||
; [intercom], which already owns an exact-match "exten => <ext>,1,..." per
|
||||
; device for CALLS, regenerated fresh on every dialplan rebuild — a
|
||||
; competing priority-1 declaration for the same extension number here would
|
||||
; race that and risk breaking normal internal calling. This context ONLY
|
||||
; ever receives MESSAGE requests, never calls.
|
||||
[sip-messaging]
|
||||
exten => _X.,1,NoOp(SIP MESSAGE to ${EXTEN})
|
||||
same => n,Set(FROM_URI=${MESSAGE(from)})
|
||||
same => n,Set(FROM_PART=${CUT(FROM_URI,@,1)})
|
||||
same => n,Set(FROM_EXT=${CUT(FROM_PART,:,2)})
|
||||
same => n,Set(SENDER_OK=${AST_CONFIG(pstn-permissions.conf,${FROM_EXT},messaging)})
|
||||
same => n,GotoIf($["${SENDER_OK}" = "yes"]?deliver:deny)
|
||||
same => n(deliver),MessageSend(pjsip:${EXTEN},${FROM_URI})
|
||||
same => n,Hangup()
|
||||
same => n(deny),NoOp(Denied — extension ${FROM_EXT} is not messaging-enabled)
|
||||
same => n,Hangup()
|
||||
EOF
|
||||
}
|
||||
|
||||
_asterisk_do_remove_presence_timer() {
|
||||
systemctl disable --now asterisk-presence-alert.timer 2>/dev/null || true
|
||||
rm -f /etc/systemd/system/asterisk-presence-alert.timer /etc/systemd/system/asterisk-presence-alert.service
|
||||
rm -f /etc/cron.d/asterisk-presence-alert
|
||||
systemctl daemon-reload 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Interactive step — called from both the fresh-install flow and "update in
|
||||
# place" (always asked either way, same reasoning as pstn-trunk.sh's
|
||||
# international-calling step: this is a live-editable extra, not a
|
||||
# structural setting, so it doesn't belong exclusively to one path).
|
||||
_asterisk_do_run_presence_step() {
|
||||
local EA_DIR="$1"
|
||||
local SETTINGS_FILE="$EA_DIR/.presence-alert.env"
|
||||
local STATE_FILE="$EA_DIR/.presence-alert.state"
|
||||
|
||||
echo ""
|
||||
local _CUR_ENABLED="n" _CUR_NTFY=""
|
||||
if [[ -f "$SETTINGS_FILE" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$SETTINGS_FILE"
|
||||
_CUR_ENABLED="${PRESENCE_ENABLED:-n}"
|
||||
_CUR_NTFY="${PRESENCE_NTFY_URL:-}"
|
||||
fi
|
||||
|
||||
if [[ "$_CUR_ENABLED" == "y" ]]; then
|
||||
echo " Extension online/offline ntfy alerts are ON (topic: $_CUR_NTFY)."
|
||||
local _CHANGE=""
|
||||
prompt_yn " Change or disable this? (y/n):" "n" _CHANGE
|
||||
[[ "$_CHANGE" =~ ^[Yy]$ ]] || return 0
|
||||
local _DISABLE=""
|
||||
prompt_yn " Disable presence alerts entirely? (y/n):" "n" _DISABLE
|
||||
if [[ "$_DISABLE" =~ ^[Yy]$ ]]; then
|
||||
_asterisk_do_remove_presence_timer
|
||||
rm -f "$EA_DIR/asterisk-presence-alert.sh" "$STATE_FILE"
|
||||
cat > "$SETTINGS_FILE" << ENV
|
||||
PRESENCE_ENABLED="n"
|
||||
PRESENCE_NTFY_URL=""
|
||||
ENV
|
||||
log_success "Presence alerts disabled."
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
local _WANT=""
|
||||
prompt_yn "Send an ntfy alert when an extension's SIP registration goes offline / comes back online? (y/n):" "n" _WANT
|
||||
[[ "$_WANT" =~ ^[Yy]$ ]] || return 0
|
||||
fi
|
||||
|
||||
local _ntfy_default="${_CUR_NTFY:-https://ntfy.sh/asterisk-presence}"
|
||||
if [[ -z "$_CUR_NTFY" ]] && [[ -f "$DOCKER_DIR/ntfy/config/server.yml" ]]; then
|
||||
local _local_base_url
|
||||
_local_base_url="$(grep -oP '(?<=base-url: ")[^"]+' "$DOCKER_DIR/ntfy/config/server.yml" 2>/dev/null || true)"
|
||||
if [[ -n "$_local_base_url" ]] && [[ "$_local_base_url" != "https://ntfy.example.com" ]]; then
|
||||
_ntfy_default="${_local_base_url}/asterisk-presence"
|
||||
log_info "Detected a configured local ntfy instance at $_local_base_url — using it as the default."
|
||||
fi
|
||||
fi
|
||||
local PRESENCE_NTFY_URL=""
|
||||
prompt_text " ntfy topic URL:" "$_ntfy_default" PRESENCE_NTFY_URL
|
||||
if [[ -z "$PRESENCE_NTFY_URL" ]]; then
|
||||
log_warning "No topic entered — presence alerts not enabled."
|
||||
return 0
|
||||
fi
|
||||
|
||||
_asterisk_do_write_presence_alert_script "$EA_DIR/asterisk-presence-alert.sh" "easy-asterisk-do" "$PRESENCE_NTFY_URL" "$STATE_FILE"
|
||||
_asterisk_do_install_presence_timer "$EA_DIR"
|
||||
|
||||
cat > "$SETTINGS_FILE" << ENV
|
||||
PRESENCE_ENABLED="y"
|
||||
PRESENCE_NTFY_URL="${PRESENCE_NTFY_URL}"
|
||||
ENV
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$SETTINGS_FILE" 2>/dev/null || true
|
||||
log_success "Presence alerts enabled (checked every 2 minutes) — topic: $PRESENCE_NTFY_URL"
|
||||
log_info "Fires only on a state CHANGE, never every check — the first check after enabling"
|
||||
log_info "never alerts by itself, since there's no prior state to compare against yet."
|
||||
}
|
||||
|
||||
# ── Shared: docker-compose.yml ─────────────────────────────────────────────
|
||||
# Same reasoning as above — one copy of the template used by both fresh
|
||||
# installs and updates. Must be called with $PWD already at $EA_DIR.
|
||||
@@ -380,6 +708,14 @@ install_asterisk-digital-ocean() {
|
||||
echo "[DRY-RUN] Would reverse-proxy the web admin on the SAME FQDN used for SIP if Caddy is already installed (needed for cert sync)"
|
||||
echo "[DRY-RUN] Would offer local OR remote Authelia to protect the web admin, if either is already available"
|
||||
echo "[DRY-RUN] Would offer 'update in place' instead of a fresh install if $EA_DIR already exists"
|
||||
echo "[DRY-RUN] Would offer optional ntfy alerts on extension registration going offline/online"
|
||||
echo "[DRY-RUN] (checked every 2 minutes via systemd timer, cron.d fallback; always asked,"
|
||||
echo "[DRY-RUN] update mode included)"
|
||||
echo "[DRY-RUN] Would patch vendor device-creation code + extensions.conf generator to route"
|
||||
echo "[DRY-RUN] internal SIP MESSAGE through a dedicated [sip-messaging] dialplan context,"
|
||||
echo "[DRY-RUN] gated live on each sender's 'messaging' flag in pstn-permissions.conf (the"
|
||||
echo "[DRY-RUN] same file/flag the Security Dashboard's checkbox writes) — independent of"
|
||||
echo "[DRY-RUN] whether the PSTN trunk is installed; migrates any already-existing devices too"
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -403,6 +739,11 @@ install_asterisk-digital-ocean() {
|
||||
_asterisk_do_refresh_vendor_files
|
||||
_asterisk_do_write_compose
|
||||
_asterisk_do_write_logrotate "$EA_DIR"
|
||||
_asterisk_do_patch_messaging_vendor_files "$EA_DIR"
|
||||
_asterisk_do_write_messaging_dialplan "$EA_DIR/config/asterisk/messaging-dialplan.conf"
|
||||
_asterisk_do_migrate_existing_devices_message_context "$EA_DIR/config/asterisk/pjsip.conf"
|
||||
ensure_docker_dir_ownership "$EA_DIR/config/asterisk"
|
||||
chmod 644 "$EA_DIR/config/asterisk/messaging-dialplan.conf"
|
||||
|
||||
log_info "Rebuilding and restarting containers..."
|
||||
if docker compose up -d --build --force-recreate; then
|
||||
@@ -411,6 +752,8 @@ install_asterisk-digital-ocean() {
|
||||
log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs"
|
||||
fi
|
||||
|
||||
_asterisk_do_run_presence_step "$EA_DIR"
|
||||
|
||||
local _EXISTING_DOMAIN _EXISTING_PORT
|
||||
_EXISTING_DOMAIN="$(grep -E '^DOMAIN_NAME=' .env | cut -d= -f2-)"
|
||||
_EXISTING_PORT="$(grep -E '^WEB_ADMIN_PORT=' .env | cut -d= -f2-)"
|
||||
@@ -472,6 +815,10 @@ install_asterisk-digital-ocean() {
|
||||
|
||||
_asterisk_do_refresh_vendor_files
|
||||
_asterisk_do_write_logrotate "$EA_DIR"
|
||||
_asterisk_do_patch_messaging_vendor_files "$EA_DIR"
|
||||
_asterisk_do_write_messaging_dialplan "$EA_DIR/config/asterisk/messaging-dialplan.conf"
|
||||
ensure_docker_dir_ownership "$EA_DIR/config/asterisk"
|
||||
chmod 644 "$EA_DIR/config/asterisk/messaging-dialplan.conf"
|
||||
|
||||
# ── DigitalOcean droplet detection ────────────────────────────────────────
|
||||
# A droplet's own public IP/ID are readable, unauthenticated, from the
|
||||
@@ -839,6 +1186,9 @@ CADDY_BLOCK
|
||||
log_info "It auto-detects this asterisk-digital-ocean install and wires up SIP protection on its own."
|
||||
fi
|
||||
|
||||
# ── Extension presence (online/offline) ntfy alerts ────────────────────────
|
||||
_asterisk_do_run_presence_step "$EA_DIR"
|
||||
|
||||
# ── README ────────────────────────────────────────────────────────────────
|
||||
write_readme "$EA_DIR" << MD
|
||||
# Easy Asterisk PBX + coturn — DigitalOcean droplet edition
|
||||
@@ -921,6 +1271,32 @@ plan for the admin panel.
|
||||
| 10000–20000 | UDP | RTP media streams |
|
||||
| 49152–49252 | UDP | TURN relay media ports |
|
||||
|
||||
## Internal SIP messaging (no PSTN trunk needed)
|
||||
|
||||
Every extension can send/receive Asterisk's native SIP MESSAGE (no carrier
|
||||
SMS, no PSTN, no cost) once its "messaging" flag is set to yes in
|
||||
\`pstn-permissions.conf\` — via the Security Dashboard's "Internal SIP
|
||||
messaging" card, or by hand. This works independent of \`pstn-trunk.sh\`
|
||||
entirely. Under the hood: every device endpoint gets
|
||||
\`message_context=sip-messaging\`, routing messages to a dedicated
|
||||
\`config/asterisk/messaging-dialplan.conf\` context instead of \`[intercom]\`
|
||||
(which already owns per-device call routing) — this install/update patches
|
||||
both the device-creation code (so new extensions pick it up automatically)
|
||||
and any devices that already existed. Confirmed against a live install's
|
||||
\`pjsip.conf\`/\`extensions.conf\` on 2026-07-23 (message_context falls back to
|
||||
context=intercom, one exact-match dialplan entry per device) — the MESSAGE
|
||||
sender-extraction logic itself is still unconfirmed against real traffic;
|
||||
if messages silently don't arrive, check
|
||||
\`docker exec easy-asterisk-do asterisk -rx "core set verbose 3"\` while
|
||||
sending one.
|
||||
|
||||
## Extension presence (online/offline) alerts
|
||||
|
||||
Optional ntfy alert when an extension's SIP registration changes state —
|
||||
offered on both fresh install and "update in place". Checked every 2
|
||||
minutes (systemd timer, cron.d fallback); fires only on a change, never on
|
||||
every check.
|
||||
|
||||
## Other services (installed separately, not by this script)
|
||||
|
||||
This installer only sets up Asterisk + coturn. Everything else — Caddy,
|
||||
|
||||
+56
-5
@@ -110,6 +110,41 @@ install_crowdsec() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ── Existing install? Offer update-in-place instead of a full reconfigure ─
|
||||
# Everything below (ASN exemptions, geo-allowlist, ntfy, remote LAPI) is
|
||||
# additive/idempotent at the file level, but there was no gate at all
|
||||
# here before — every rerun re-asked all four optional questions
|
||||
# unconditionally, which reads as "reconfigure" rather than "update" even
|
||||
# though nothing already-configured was actually being destroyed.
|
||||
local _REMOTE_LAPI_PENDING=""
|
||||
local ASK_OPTIONAL=true
|
||||
if command -v cscli &> /dev/null; then
|
||||
echo " CrowdSec is already installed."
|
||||
local CS_MODE=""
|
||||
prompt_reinstall_mode CS_MODE 2>/dev/null || {
|
||||
# prompt_reinstall_mode isn't defined in the standalone stub — fall
|
||||
# back to a plain yes/no when run outside the full repo.
|
||||
local _r=""
|
||||
prompt_yn " Reconfigure the optional settings below (ASN exemptions, geo-allowlist, ntfy, remote LAPI)? (y/n):" "n" _r
|
||||
[ "$_r" = "y" ] || [ "$_r" = "Y" ] && CS_MODE="fresh" || CS_MODE="update"
|
||||
}
|
||||
case "$CS_MODE" in
|
||||
update)
|
||||
ASK_OPTIONAL=false
|
||||
log_info "Refreshing agent/collections/acquisitions only — ASN exemptions,"
|
||||
log_info "geo-allowlist, ntfy alerts, and remote-LAPI settings are left exactly as"
|
||||
log_info "they are. Choose 'fresh' instead to revisit any of those."
|
||||
;;
|
||||
cancel)
|
||||
log_info "Leaving the existing CrowdSec install as-is."
|
||||
return 0
|
||||
;;
|
||||
fresh)
|
||||
log_info "Proceeding with a full reconfigure — every optional prompt below runs again."
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# ── 1. Install the CrowdSec agent ────────────────────────────────────────
|
||||
if command -v cscli &> /dev/null; then
|
||||
echo " ✓ CrowdSec is already installed"
|
||||
@@ -198,7 +233,9 @@ labels:
|
||||
# the hub originals so there's no double-processing of the same events.
|
||||
echo ""
|
||||
local ASN_EXEMPT=""
|
||||
prompt_yn "Exempt specific carrier ASNs from Asterisk brute-force bans only? (y/n):" "n" ASN_EXEMPT
|
||||
if [ "$ASK_OPTIONAL" = true ]; then
|
||||
prompt_yn "Exempt specific carrier ASNs from Asterisk brute-force bans only? (y/n):" "n" ASN_EXEMPT
|
||||
fi
|
||||
if [ "$ASN_EXEMPT" = "y" ] || [ "$ASN_EXEMPT" = "Y" ]; then
|
||||
sudo cscli collections install crowdsecurity/geoip-enrich 2>/dev/null || true
|
||||
echo " ASNs observed live: T-Mobile 21928, Starlink 14593. A carrier can operate"
|
||||
@@ -295,7 +332,9 @@ ASTENUM
|
||||
echo " https://app.crowdsec.net/"
|
||||
echo ""
|
||||
local GEO_ALLOWLIST=""
|
||||
prompt_yn "Restrict Caddy-fronted web traffic to North America + Europe only (block every other country)? Does NOT affect SSH. (y/n):" "n" GEO_ALLOWLIST
|
||||
if [ "$ASK_OPTIONAL" = true ]; then
|
||||
prompt_yn "Restrict Caddy-fronted web traffic to North America + Europe only (block every other country)? Does NOT affect SSH. (y/n):" "n" GEO_ALLOWLIST
|
||||
fi
|
||||
if [ "$GEO_ALLOWLIST" = "y" ] || [ "$GEO_ALLOWLIST" = "Y" ]; then
|
||||
echo " Installing geoip-enrich (tags every event with a country code; no MaxMind"
|
||||
echo " account needed — CrowdSec bundles its own redistributable GeoLite2 data)..."
|
||||
@@ -353,7 +392,9 @@ labels:
|
||||
# ── 7. Optional: push ban alerts to ntfy ─────────────────────────────────
|
||||
echo ""
|
||||
local CS_NTFY=""
|
||||
prompt_yn "Send CrowdSec ban alerts to an ntfy topic? (y/n):" "n" CS_NTFY
|
||||
if [ "$ASK_OPTIONAL" = true ]; then
|
||||
prompt_yn "Send CrowdSec ban alerts to an ntfy topic? (y/n):" "n" CS_NTFY
|
||||
fi
|
||||
if [ "$CS_NTFY" = "y" ] || [ "$CS_NTFY" = "Y" ]; then
|
||||
# Prefer a locally-installed ntfy's own base-url as the default, if one
|
||||
# exists and actually looks configured (not still the placeholder
|
||||
@@ -420,8 +461,10 @@ headers:
|
||||
# of every box running its own. Useful if you already have CrowdSec on
|
||||
# a homelab and don't want a second LAPI+SQLite DB on this droplet.
|
||||
echo ""
|
||||
local USE_REMOTE_LAPI="" _REMOTE_LAPI_PENDING=""
|
||||
prompt_yn "Point this agent at a remote/central LAPI instead of running its own (e.g. one already on a homelab)? (y/n):" "n" USE_REMOTE_LAPI
|
||||
local USE_REMOTE_LAPI=""
|
||||
if [ "$ASK_OPTIONAL" = true ]; then
|
||||
prompt_yn "Point this agent at a remote/central LAPI instead of running its own (e.g. one already on a homelab)? (y/n):" "n" USE_REMOTE_LAPI
|
||||
fi
|
||||
if [ "$USE_REMOTE_LAPI" = "y" ] || [ "$USE_REMOTE_LAPI" = "Y" ]; then
|
||||
echo ""
|
||||
echo " This registers this machine and disables its local API server."
|
||||
@@ -506,6 +549,14 @@ install. The real configuration lives under `/etc/crowdsec`.
|
||||
before they ever touch your services.
|
||||
- Optionally enriches events with **geo/ASN** data for geo-blocking.
|
||||
|
||||
## Rerunning this script
|
||||
|
||||
Rerunning offers **update** (refreshes the agent/collections/acquisitions
|
||||
only — ASN exemptions, geo-allowlist, ntfy, and remote-LAPI settings are left
|
||||
exactly as they are) or **fresh** (revisit every optional prompt again, same
|
||||
as a first install). Nothing here is destructive either way — "fresh" only
|
||||
overwrites a setting if you actually answer its prompt differently.
|
||||
|
||||
## Key commands
|
||||
|
||||
```
|
||||
|
||||
+28
-1
@@ -260,12 +260,26 @@ base-url: "${NTFY_BASE_URL:-https://ntfy.example.com}" # UPDATE to your actual
|
||||
cache-file: /var/cache/ntfy/cache.db
|
||||
cache-duration: 12h
|
||||
auth-file: /var/cache/ntfy/auth.db
|
||||
auth-default-access: deny-all
|
||||
# read-write, not deny-all: with an auth-file present, ntfy auth-checks every
|
||||
# request, and deny-all blocks anonymous publish/subscribe on EVERY topic
|
||||
# unless you separately create a user and grant a per-topic ACL (nothing in
|
||||
# this installer, crowdsec.sh, or pstn-trunk.sh does that) — every ntfy alert
|
||||
# they curl to a topic silently 403s, and a plain phone subscription gets
|
||||
# nothing, with no visible error either side. read-write matches public
|
||||
# ntfy.sh's own model instead: anyone who knows/guesses the topic name can
|
||||
# read/publish it, so treat topic names as a shared secret (long/random for
|
||||
# anything sensitive) rather than relying on auth you'd have to wire up
|
||||
# per-script. Confirmed live: this was the actual cause of a self-hosted
|
||||
# instance never delivering alerts despite a correct topic subscription.
|
||||
auth-default-access: read-write
|
||||
behind-proxy: true
|
||||
NTFY_CFG
|
||||
[[ -n "$NTFY_BASE_URL" ]] \
|
||||
&& log_info "base-url set to $NTFY_BASE_URL — update if domain changes" \
|
||||
|| log_warning "base-url set to placeholder — edit config/server.yml after install"
|
||||
log_info "auth-default-access: read-write — topics are open to anyone who knows the"
|
||||
log_info "topic name (same model as public ntfy.sh). Use long/random topic names for"
|
||||
log_info "anything sensitive, or lock down individual topics later with 'ntfy access'."
|
||||
fi
|
||||
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$NTFY_DIR"
|
||||
@@ -288,6 +302,19 @@ phone or browser.
|
||||
- Send a notification: \`curl -d "Hello!" localhost:8090/mytopic\`
|
||||
- Subscribe on phone: ntfy app -> Add subscription -> localhost:8090/mytopic
|
||||
|
||||
## Access model
|
||||
\`auth-default-access: read-write\` — anyone who knows a topic name can read
|
||||
and publish to it, same as public ntfy.sh. There's no per-script login wired
|
||||
up (crowdsec.sh, pstn-trunk.sh, etc. all just \`curl -d ... \$topic_url\` with
|
||||
no auth), so this is what makes those alerts actually arrive. Treat topic
|
||||
names as a shared secret — use long/random ones for anything sensitive — or
|
||||
lock a specific topic down individually:
|
||||
\`\`\`
|
||||
docker exec ntfy ntfy user add myuser
|
||||
docker exec ntfy ntfy access myuser mytopic read-write
|
||||
docker exec ntfy ntfy access everyone mytopic deny
|
||||
\`\`\`
|
||||
|
||||
## Data
|
||||
- Config: ./config (mounted to /etc/ntfy)
|
||||
- Cache: ./cache (mounted to /var/cache/ntfy)
|
||||
|
||||
+101
-11
@@ -141,7 +141,7 @@ 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"
|
||||
_secdash_configure_caddy "$DASHBOARD_PORT" "$ASTERISK_ADMIN_URL"
|
||||
fi
|
||||
return 0
|
||||
;;
|
||||
@@ -184,7 +184,7 @@ 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"
|
||||
_secdash_configure_caddy "$DASHBOARD_PORT" "$ASTERISK_ADMIN_URL"
|
||||
|
||||
write_readme "$APP_DIR" << README_MD
|
||||
# Security Dashboard
|
||||
@@ -236,7 +236,20 @@ not in Docker — it needs to call \`cscli\` and read Asterisk's log directly.
|
||||
and international-calling allow-list are deliberately **not** managed
|
||||
here — CLI-only, via \`sudo ./setup.sh pstn-trunk\` — since both are more
|
||||
security-sensitive than what this tab already exposes.
|
||||
- Link to the Asterisk web admin itself (doesn't embed it, just links out).
|
||||
- **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. Only
|
||||
shows up 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.
|
||||
|
||||
## Manage
|
||||
\`\`\`
|
||||
@@ -378,7 +391,7 @@ 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"
|
||||
local DASHBOARD_PORT="$1" ADMIN_URL="${2:-}"
|
||||
|
||||
echo ""
|
||||
if ! command -v docker &>/dev/null || ! docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^caddy$"; then
|
||||
@@ -509,6 +522,66 @@ CADDYBLOCK
|
||||
ufw_allow_from_caddy_net "${DASHBOARD_PORT}"
|
||||
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."
|
||||
else
|
||||
log_warning "Caddyfile patched, but reload/restart failed — check: docker logs caddy"
|
||||
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
|
||||
}
|
||||
|
||||
# Removes the dashboard's existing Caddyfile site block (found via its
|
||||
@@ -1433,8 +1506,8 @@ INDEX_HTML = """<!doctype html>
|
||||
<button class="tab-btn active" data-tab="security">Security Log</button>
|
||||
<button class="tab-btn" data-tab="crowdsec">CrowdSec</button>
|
||||
<button class="tab-btn" data-tab="pstn">PSTN Trunk</button>
|
||||
<button class="tab-btn" id="asterisk-tab-btn" data-tab="asterisk" style="display:none">Asterisk Admin</button>
|
||||
</nav>
|
||||
<a id="admin-link" href="#" target="_blank" style="display:none">Asterisk Web Admin ↗</a>
|
||||
</header>
|
||||
<main>
|
||||
<div id="tab-security">
|
||||
@@ -1480,7 +1553,7 @@ INDEX_HTML = """<!doctype html>
|
||||
<div class="card">
|
||||
<h3 style="margin-top:0">Internal SIP messaging</h3>
|
||||
<p class="muted">
|
||||
Asterisk's native SIP texting between extensions — no carrier SMS, no PSTN, no cost, and no dependency on a PSTN trunk being installed at all. Independent of the calling permissions below. Note: this flag is live-editable here, but whether Asterisk actually delivers/gates messages using it depends on dialplan wiring not yet verified against a live install.
|
||||
Asterisk's native SIP texting between extensions — no carrier SMS, no PSTN, no cost, and no dependency on a PSTN trunk being installed at all. Independent of the calling permissions below. Enforced live by a dedicated dialplan context (see services/asterisk-digital-ocean.sh's README) — install/rerun that service to pick up the dialplan wiring if this box predates it.
|
||||
</p>
|
||||
<table id="msg-table"><thead><tr><th>Ext</th><th>Name</th><th>Enabled</th><th></th></tr></thead><tbody></tbody></table>
|
||||
<div id="msg-msg" class="muted" style="margin-top:0.5rem"></div>
|
||||
@@ -1522,7 +1595,7 @@ INDEX_HTML = """<!doctype html>
|
||||
Changes apply live, on the next call — no Asterisk restart needed.
|
||||
</p>
|
||||
<p class="muted">
|
||||
<b>Messaging</b> — Asterisk's native internal SIP texting (no carrier SMS, no PSTN, no cost), independent of the calling tier. Note: this flag is live-editable here, but whether Asterisk actually delivers/gates messages using it depends on dialplan wiring not yet verified against a live install — see this service's README.
|
||||
<b>Messaging</b> — Asterisk's native internal SIP texting (no carrier SMS, no PSTN, no cost), independent of the calling tier. Enforced live by a dedicated dialplan context — see services/asterisk-digital-ocean.sh's README for how, and its caveat on the sender-extraction logic still needing real-traffic confirmation.
|
||||
</p>
|
||||
<table id="pstn-table"><thead><tr><th>Ext</th><th>Name</th><th>Tier</th><th>Approved numbers (restricted only)</th><th>Messaging</th><th></th></tr></thead><tbody></tbody></table>
|
||||
<div id="pstn-msg" class="muted" style="margin-top:0.5rem"></div>
|
||||
@@ -1542,17 +1615,35 @@ INDEX_HTML = """<!doctype html>
|
||||
</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", "crowdsec", "pstn"];
|
||||
const TABS = ["security", "crowdsec", "pstn", "asterisk"];
|
||||
document.querySelectorAll(".tab-btn").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
TABS.forEach(t => { document.getElementById("tab-" + t).style.display = btn.dataset.tab === t ? "" : "none"; });
|
||||
if (btn.dataset.tab === "pstn") { loadPstnStatus(); loadMessaging(); loadGroups(); }
|
||||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1933,9 +2024,8 @@ async function removePersonalDid(did) {
|
||||
|
||||
const adminUrl = "__ASTERISK_ADMIN_URL__";
|
||||
if (adminUrl) {
|
||||
const link = document.getElementById("admin-link");
|
||||
link.href = adminUrl;
|
||||
link.style.display = "";
|
||||
document.getElementById("asterisk-tab-btn").style.display = "";
|
||||
document.getElementById("admin-link-fallback").href = adminUrl;
|
||||
}
|
||||
|
||||
loadSecurity();
|
||||
|
||||
Reference in New Issue
Block a user