From 9390382bf4a5ed2d96e5f47765971ff1312a9915 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 03:30:13 +0000 Subject: [PATCH] Add Sipnetic QR-scan provisioning and a kiosk client installer download Two concrete asks: "a QR code generator for sipnetic... displayed on the security website" and a real download for the baresip kiosk client instead of just pointing at docs. Sipnetic QR: ea_device_sipnetic_string() builds Sipnetic's own documented account-string format (n=/u=/d=/p=/dt=, semicolon-separated, from https://www.sipnetic.com/qr-codes) from the exact same data the existing device-details panel already reads back out of pjsip.conf -- unlike ea_device_provisioning()'s deliberately generic plain-text file (written because no vendor XML format could be verified), this one has real documentation to build against. Rendered client-side with davidshimjs/qrcodejs (MIT, wraps Kazuhiko Arase's original QRCode for JavaScript) embedded verbatim with its license header intact -- no CDN call, no new Python dependency, same self-contained approach as the rest of this page. New "Sipnetic QR code" button on each extension's detail panel; verified the embedded library actually renders (not just parses) via a real jsdom run producing real QR cell output. Kiosk installer download: _secdash_copy_kiosk_installer copies vendor/easy-asterisk/easy-asterisk-v0.10.0.sh alongside the deployed app at install time (both update and fresh-install paths) and a new /download/kiosk-client-installer.sh route serves it -- linked directly from the Ring Groups card's help text instead of only being reachable by manually finding the file in the repo. Copied at install time rather than read live, since a standalone run of this one file (no full repo clone -- explicitly supported here) has no vendor/ directory to read from; missing source degrades to a clean 404 on that one link, not an install failure. Verified: bash -n, py_compile on the extracted embedded app.py, node --check on the extracted embedded JS (including the newly-embedded QR library), and the account-string output checked directly against Sipnetic's own documented example format. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww --- services/security-dashboard.sh | 739 ++++++++++++++++++++++++++++++++- 1 file changed, 737 insertions(+), 2 deletions(-) diff --git a/services/security-dashboard.sh b/services/security-dashboard.sh index 239f973..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. @@ -3250,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.

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