diff --git a/services/security-dashboard.sh b/services/security-dashboard.sh index 2bd99ca..0834055 100644 --- a/services/security-dashboard.sh +++ b/services/security-dashboard.sh @@ -140,6 +140,7 @@ install_security-dashboard() { _secdash_grant_asterisk_access "$SVC_USER" "$ASTERISK_LOG_DIR" "$ASTERISK_CONFIG_DIR" "$ASTERISK_EA_CONFIG_DIR" _secdash_write_app "$APP_DIR" _secdash_write_asn_helper "$APP_DIR" + _secdash_copy_kiosk_installer "$APP_DIR" _secdash_write_sudoers "$SVC_USER" "$ASTERISK_EA_CONTAINER" _secdash_write_systemd_unit "$APP_DIR" "$SVC_USER" "$DASHBOARD_PORT" "$ASTERISK_LOG_DIR" "$ASTERISK_CONFIG_DIR" "$ASTERISK_EA_CONFIG_DIR" "$ASTERISK_EA_CONTAINER" # systemd caches unit files; a plain restart re-runs the OLD @@ -182,6 +183,7 @@ install_security-dashboard() { _secdash_write_app "$APP_DIR" chown -R "$SVC_USER:$SVC_USER" "$APP_DIR" _secdash_write_asn_helper "$APP_DIR" + _secdash_copy_kiosk_installer "$APP_DIR" _secdash_write_sudoers "$SVC_USER" "$ASTERISK_EA_CONTAINER" _secdash_write_systemd_unit "$APP_DIR" "$SVC_USER" "$DASHBOARD_PORT" "$ASTERISK_LOG_DIR" "$ASTERISK_CONFIG_DIR" "$ASTERISK_EA_CONFIG_DIR" "$ASTERISK_EA_CONTAINER" @@ -824,6 +826,30 @@ ASNHELPER chmod 700 "$_app_dir/set-asn-exempt.sh" } +# Copies the vendored Easy Asterisk installer alongside the app so the +# dashboard's "Download kiosk client installer" link (Ring Groups card -> +# "How this works") has a real file to serve -- see docs/kiosk-paging-setup.md +# for what it's for. Copied at install time, not read live from vendor/ at +# request time, since a standalone run of this one file (`sudo bash +# security-dashboard.sh`, no full repo clone -- this file supports that mode, +# see the _RUN_STANDALONE stub above) has no vendor/ directory to read from at +# all. Missing source is a soft failure: the rest of the dashboard installs +# fine either way, only that one download link won't work. +_secdash_copy_kiosk_installer() { + local _app_dir="$1" + local _self_dir + _self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + local _vendor_script="$_self_dir/../vendor/easy-asterisk/easy-asterisk-v0.10.0.sh" + if [[ -f "$_vendor_script" ]]; then + cp "$_vendor_script" "$_app_dir/kiosk-client-installer.sh" + chmod 644 "$_app_dir/kiosk-client-installer.sh" + else + log_warning "vendor/easy-asterisk/easy-asterisk-v0.10.0.sh not found (standalone run without" + log_warning "the full repo?) -- the dashboard's kiosk installer download link will 404 until" + log_warning "this exists. Re-run from a full 'ubuntu-post-install' checkout to fix." + fi +} + # Writes the Python app. Separate function so "update" mode (refresh code, # keep config) and fresh installs share one copy instead of drifting apart. _secdash_write_app() { @@ -859,6 +885,12 @@ ASN_SCENARIO_FILES = [ # goes through sudo instead of loosening those files' permissions. ASN_HELPER_SCRIPT = "/opt/security-dashboard/set-asn-exempt.sh" +# Copied in by _secdash_copy_kiosk_installer at install time (see that +# function's own comment for why this reads a local copy, not vendor/ live). +# May not exist on a standalone run without the full repo -- the download +# route below 404s cleanly in that case rather than erroring. +KIOSK_INSTALLER_SCRIPT = "/opt/security-dashboard/kiosk-client-installer.sh" + TS_RE = re.compile(r"^\[([^\]]+)\]") KV_RE = re.compile(r'(\w+)="([^"]*)"') ASN_FILTER_RE = re.compile(r"ASNNumber in \[([^\]]*)\]\)") @@ -2315,6 +2347,33 @@ def ea_device_provisioning(extension): re.sub(r"[^A-Za-z0-9._-]", "-", host), d["extension"]), "\n".join(lines) + "\n" +def ea_device_sipnetic_string(extension): + """Sipnetic's own documented "account string" QR-scan format + (https://www.sipnetic.com/qr-codes): semicolon-separated key=value pairs, + n=display name, u=username, d=domain/IP (no port), p=password, + dt=default transport (0=UDP, 1=TCP, 2=TLS). Unlike + ea_device_provisioning()'s deliberately generic plain-text file, this one + IS a verified, documented format for one specific app, built from the + exact same ea_device_details() data. + + A literal ';' in any field must be doubled per that same doc page -- + generated passwords are alnum-only (_ea_generate_password) so this only + matters for a hand-typed device name, but escaping costs nothing.""" + d = ea_device_details(extension) + if not d: + return None + + def esc_field(v): + return str(v or "").replace(";", ";;") + + host = d["server"] or "" + dt = "2" if d["transport"] == "TLS" else "0" + return "n=%s;u=%s;d=%s;p=%s;dt=%s;" % ( + esc_field(d["name"] or d["extension"]), esc_field(d["extension"]), + esc_field(host), esc_field(d["password"]), dt, + ) + + def _ea_edit_device_block(extension, mutate): """Rewrite one device's endpoint stanza in place. @@ -2721,6 +2780,44 @@ def ea_rename_room(extension, new_name): return True, "Room renamed" +def ea_update_room_settings(extension, room_type, timeout): + """Changes an existing room's type (ring/page) and timeout without + touching its name, members, or any DID assignment — the one thing + creating or renaming a room couldn't already do: change these two + fields after the room exists, rather than only at creation time.""" + path = _ea_rooms_host_path() + if not path or not os.path.isfile(path): + return False, "Rooms file not found" + room_type = (room_type or "").strip() + if room_type not in ("ring", "page"): + return False, "Type must be 'ring' or 'page'" + timeout = str(timeout or "").strip() + if not timeout.isdigit() or int(timeout) <= 0: + return False, "Timeout must be a positive number of seconds" + with open(path) as f: + lines = f.readlines() + new_lines = [] + found = False + for line in lines: + stripped = line.strip() + if stripped and not stripped.startswith("#"): + parts = stripped.split("|") + if len(parts) >= 5 and parts[0] == extension: + parts[3] = timeout + parts[4] = room_type + new_lines.append("|".join(parts) + "\n") + found = True + continue + new_lines.append(line) + if not found: + return False, "Room not found" + ok, err = ea_docker_write(EA_ROOMS_CONTAINER_PATH, "".join(new_lines)) + if not ok: + return False, err + ea_rebuild_dialplan() + return True, "Room settings updated" + + def _ea_update_room_members(extension, new_members): path = _ea_rooms_host_path() if not path or not os.path.isfile(path): @@ -3212,8 +3309,9 @@ INDEX_HTML = """
Ring vs Page, and mixing auto-answer devices in one group

Ring dials every member at once — first to answer gets the call, everyone else stops ringing. Page tells Asterisk to signal auto-answer to every member via SIP headers, for devices that honor it, turning the same simultaneous dial into a one-way intercom-style broadcast instead.

-

You don't need Page just to mix an auto-answering device with normally-ringing phones in the same group, though — auto-answer is really a property of the device's own SIP client configuration, not something Asterisk enforces per member. A plain Ring group already dials everyone simultaneously, so a device configured to auto-answer (e.g. a dedicated intercom/kiosk running baresip in Answer Mode: Auto) picks up instantly, while ordinary phones in the same group just keep ringing until a person answers — no extra setting needed here for that mix.

-

For a dedicated always-on auto-answer device (a wall-mounted intercom, a paging station), Easy Asterisk — the vendor project this installer builds on — has a built-in baresip-based kiosk client for exactly that. It installs on a separate small Linux machine (an old PC, a Raspberry Pi), not this Asterisk server itself. See docs/kiosk-paging-setup.md in this repo for the full walkthrough.

+

You don't need Page just to mix an auto-answering device with normally-ringing phones in the same group, though — auto-answer is really a property of the device's own SIP client configuration, not something Asterisk enforces per member. Confirmed against baresip's own source: it decides purely from its account's local answermode setting and never looks at any auto-answer signal on the incoming call, so a device configured to auto-answer (e.g. a dedicated intercom/kiosk running baresip in Answer Mode: Auto) picks up everything routed to it instantly and unconditionally, while ordinary phones in the same plain Ring group just keep ringing until a person answers — no extra setting needed here for that mix.

+

For a dedicated always-on auto-answer device (a wall-mounted intercom, a paging station), Easy Asterisk — the vendor project this installer builds on — has a built-in baresip-based kiosk client for exactly that. It installs on a separate small Linux machine (an old PC, a Raspberry Pi), not this Asterisk server itself: download the installer script, then see docs/kiosk-paging-setup.md in this repo for the full walkthrough.

+

For a phone or tablet running Sipnetic instead, each extension's own detail panel below (Extensions tab → click a row → "Sipnetic QR code") can generate a scan-to-configure code — no dedicated kiosk hardware needed for that one.

@@ -3270,6 +3368,625 @@ INDEX_HTML = """