#!/bin/bash # ================================================================ # Easy Asterisk - Interactive Installer v0.10.0 # # Copyright (C) 2025 Easy Asterisk Contributors # Licensed under GNU General Public License v3.0 # See LICENSE file or https://www.gnu.org/licenses/gpl-3.0.html # # UPDATES in v0.10.0: # - FIXED: Extension deletion now properly removes all sections (endpoint, auth, aor) # - FIXED: Extension renaming now preserves AA tags correctly # - FIXED: LAN/VPN devices now explicitly use UDP transport (prevents TLS fallback) # - FIXED: LAN devices now have media_encryption=no to prevent SRTP issues # - ADDED: Web Admin interface for browser-based client management # - View device status (online/offline) in real-time # - Add/delete devices via web interface # - View rooms and categories # - HTTP Basic authentication with SHA256 password hashing # - Access at http://server:8080/clients # - IMPROVED: Device deletion uses awk for reliable multi-section removal # - IMPROVED: Device renaming uses awk to handle all edge cases # # PREVIOUS UPDATES (v0.9.9): # - REMOVED: All COTURN/TURN relay server code (focus on direct connections) # - ADDED: VLAN subnet configuration to prevent 30-second call drops # - ADDED: Provisioning Manager (http.conf setup, symlinks, linphone.xml editor) # - ADDED: Manual Update System for Asterisk with backup/rollback # - ADDED: Room Directory (visual display of Ring Groups vs Page Groups) # - ADDED: Split-horizon DNS documentation for VLAN environments # - IMPROVED: Server IP address documented in transport configurations # - IMPROVED: Multiple local_net entries for proper VLAN support # ================================================================ set +e # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # Defaults DEFAULT_SIP_PORT="5060" DEFAULT_SIPS_PORT="5061" CONFIG_DIR="/etc/easy-asterisk" CONFIG_FILE="${CONFIG_DIR}/config" PTT_CONFIG_FILE="${CONFIG_DIR}/ptt-device" CATEGORIES_FILE="${CONFIG_DIR}/categories.conf" ROOMS_FILE="${CONFIG_DIR}/rooms.conf" PROVISIONING_DIR="/var/lib/asterisk/static-http" SCRIPT_VERSION="0.10.0" # ================================================================ # 1. CORE HELPER FUNCTIONS # ================================================================ print_header() { echo -e "\n${CYAN}╔══════════════════════════════════════════════════════════╗${NC}" echo -e "${CYAN} $1${NC}" echo -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}\n" } print_info() { echo -e "${GREEN}[INFO]${NC} $1"; } print_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } print_error() { echo -e "${RED}[ERROR]${NC} $1"; } print_success() { echo -e "${GREEN}[OK]${NC} $1"; } check_root() { if [[ $EUID -ne 0 ]]; then print_error "This script must be run as root (use sudo)" exit 1 fi } generate_password() { tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 16 } select_user() { # Scan /home for real users (exclude system accounts) local -a users=() local -a user_ids=() local count=0 echo "Scanning for users..." echo "" # Get users from /home with valid shells while IFS=: read -r username _ uid _ _ homedir shell; do # Only include users with UID >= 1000 and valid shell if [[ $uid -ge 1000 && -d "$homedir" && "$shell" != "/usr/sbin/nologin" && "$shell" != "/bin/false" ]]; then ((count++)) users+=("$username") user_ids+=("$uid") echo " ${count}) ${username} (UID: ${uid}, Home: ${homedir})" fi done < /etc/passwd # Add option to manually enter username ((count++)) echo " ${count}) Enter username manually" echo "" # Suggest default based on SUDO_USER or first user found local default_choice="" local default_user="${SUDO_USER:-}" if [[ -z "$default_user" ]]; then default_user="${users[0]:-}" default_choice="1" else # Find index of SUDO_USER for i in "${!users[@]}"; do if [[ "${users[$i]}" == "$default_user" ]]; then default_choice=$((i + 1)) break fi done fi if [[ -n "$default_choice" ]]; then read -p "Select user [${default_choice}]: " choice choice="${choice:-$default_choice}" else read -p "Select user: " choice fi # Validate choice if [[ "$choice" =~ ^[0-9]+$ && "$choice" -le "${#users[@]}" && "$choice" -gt 0 ]]; then local idx=$((choice - 1)) KIOSK_USER="${users[$idx]}" KIOSK_UID="${user_ids[$idx]}" echo "" print_success "Selected user: $KIOSK_USER (UID: $KIOSK_UID)" return 0 elif [[ "$choice" == "$count" ]]; then # Manual entry echo "" read -p "Enter username: " KIOSK_USER if id "$KIOSK_USER" >/dev/null 2>&1; then KIOSK_UID=$(id -u "$KIOSK_USER") print_success "Selected user: $KIOSK_USER (UID: $KIOSK_UID)" return 0 else print_error "User '$KIOSK_USER' not found" return 1 fi else print_error "Invalid selection" return 1 fi } load_config() { if [[ -f "$CONFIG_FILE" ]]; then source "$CONFIG_FILE" 2>/dev/null || true fi INSTALLED_SERVER="${INSTALLED_SERVER:-n}" INSTALLED_CLIENT="${INSTALLED_CLIENT:-n}" KIOSK_USER="${KIOSK_USER:-}" KIOSK_UID="${KIOSK_UID:-}" HAS_VLANS="${HAS_VLANS:-n}" VLAN_SUBNETS="${VLAN_SUBNETS:-}" WEB_ADMIN_PORT="${WEB_ADMIN_PORT:-8080}" return 0 } backup_config() { local file=$1 if [[ -f "$file" ]]; then cp "$file" "${file}.backup-$(date +%s)" ls -tp "${file}.backup-"* 2>/dev/null | tail -n +6 | xargs -I {} rm -- {} 2>/dev/null fi } save_config() { mkdir -p "$CONFIG_DIR" chmod 755 "$CONFIG_DIR" cat > "$CONFIG_FILE" << EOF # Easy Asterisk Configuration - $(date) KIOSK_USER="$KIOSK_USER" KIOSK_UID="$KIOSK_UID" KIOSK_EXTENSION="$KIOSK_EXTENSION" KIOSK_NAME="$KIOSK_NAME" SIP_PASSWORD="$SIP_PASSWORD" ASTERISK_HOST="$ASTERISK_HOST" DOMAIN_NAME="$DOMAIN_NAME" ENABLE_TLS="$ENABLE_TLS" HAS_VLANS="$HAS_VLANS" VLAN_SUBNETS="$VLAN_SUBNETS" CERT_PATH="$CERT_PATH" KEY_PATH="$KEY_PATH" INSTALLED_SERVER="$INSTALLED_SERVER" INSTALLED_CLIENT="$INSTALLED_CLIENT" CURRENT_PUBLIC_IP="$CURRENT_PUBLIC_IP" PTT_DEVICE="$PTT_DEVICE" PTT_KEYCODE="$PTT_KEYCODE" LOCAL_CIDR="$LOCAL_CIDR" WEB_ADMIN_PORT="$WEB_ADMIN_PORT" EOF chmod 644 "$CONFIG_FILE" # Save PTT config separately if [[ -n "$PTT_DEVICE" ]]; then cat > "$PTT_CONFIG_FILE" << EOF PTT_DEVICE="$PTT_DEVICE" PTT_KEYCODE="$PTT_KEYCODE" EOF chmod 644 "$PTT_CONFIG_FILE" fi } open_firewall_ports() { print_info "Configuring firewall ports..." if command -v ufw &>/dev/null; then if ufw status 2>/dev/null | grep -q "Status: active"; then ufw allow 5060/udp comment "SIP UDP" 2>/dev/null || true ufw allow 5061/tcp comment "SIP TLS" 2>/dev/null || true ufw allow 10000:20000/udp comment "RTP Media" 2>/dev/null || true ufw allow 8088/tcp comment "HTTP Provisioning" 2>/dev/null || true ufw allow 8089/tcp comment "HTTPS Provisioning" 2>/dev/null || true ufw reload 2>/dev/null || true print_success "UFW firewall ports opened" fi fi } # ================================================================ # 2. UTILITY FUNCTIONS # ================================================================ get_public_ip() { local ip=$(curl -s -4 --connect-timeout 5 ifconfig.me 2>/dev/null || curl -s -4 --connect-timeout 5 icanhazip.com 2>/dev/null || echo "") echo "$ip" } # ================================================================ # 3. DEVICE MANAGEMENT # ================================================================ initialize_default_categories() { mkdir -p "$CONFIG_DIR" if [[ ! -f "$CATEGORIES_FILE" ]]; then cat > "$CATEGORIES_FILE" << 'EOF' # Format: id|name|auto_answer(yes/no)|description kiosk|Kiosks|yes|Fixed auto-answer intercoms mobile|Mobile Devices|no|Phones and mobile devices EOF chmod 600 "$CATEGORIES_FILE" fi if [[ ! -f "$ROOMS_FILE" ]]; then cat > "$ROOMS_FILE" << 'EOF' # Format: ext|name|members|timeout|type(ring/page) 199|All Kiosks|101,102,103,104,105|60|page 299|All Mobile|201,202,203,204,205|60|ring EOF chmod 600 "$ROOMS_FILE" fi } list_categories() { initialize_default_categories local index=1 while IFS='|' read -r cat_id cat_name auto_answer description; do [[ "$cat_id" =~ ^# ]] && continue [[ -z "$cat_id" ]] && continue local auto_text="${RED}Ring${NC}" [[ "$auto_answer" == "yes" ]] && auto_text="${GREEN}Auto-answer${NC}" echo -e " ${CYAN}$index)${NC} ${BOLD}$cat_name${NC} ($cat_id) - $auto_text" ((index++)) done < "$CATEGORIES_FILE" } get_category_by_index() { local target_index=$1 local index=1 while IFS='|' read -r cat_id cat_name auto_answer description; do [[ "$cat_id" =~ ^# ]] && continue [[ -z "$cat_id" ]] && continue if [[ $index -eq $target_index ]]; then echo "$cat_id|$cat_name|$auto_answer" return 0 fi ((index++)) done < "$CATEGORIES_FILE" } manage_categories() { print_header "Manage Categories" list_categories echo "" echo " 1) Add Category" echo " 2) Rename Category" echo " 3) Delete Category" echo " 0) Back" read -p "Select: " choice case $choice in 1) read -p "ID (lowercase): " cid read -p "Display Name: " cname read -p "Auto Answer? [y/N]: " ca local ans="no" [[ "$ca" =~ ^[Yy]$ ]] && ans="yes" echo "${cid}|${cname}|${ans}|Custom category" >> "$CATEGORIES_FILE" print_success "Category added" rebuild_dialplan ;; 2) read -p "Number to rename: " num local data=$(get_category_by_index "$num") if [[ -z "$data" ]]; then print_error "Invalid selection" return fi local old_id=$(echo "$data" | cut -d'|' -f1) local old_name=$(echo "$data" | cut -d'|' -f2) local auto_answer=$(echo "$data" | cut -d'|' -f3) echo "Current: $old_name (ID: $old_id)" read -p "New display name: " new_name if [[ -z "$new_name" ]]; then print_error "Name cannot be empty" return fi # Backup backup_config "$CATEGORIES_FILE" # Update category file sed -i "s/^${old_id}|${old_name}|/${old_id}|${new_name}|/" "$CATEGORIES_FILE" print_success "Category renamed: ${old_name} → ${new_name}" rebuild_dialplan ;; 3) read -p "Number to delete: " num local data=$(get_category_by_index "$num") if [[ -z "$data" ]]; then print_error "Invalid selection" return fi local cid=$(echo "$data" | cut -d'|' -f1) local cname=$(echo "$data" | cut -d'|' -f2) # Count devices in this category local device_count=$(grep -c "; === Device:.* (${cid})" /etc/asterisk/pjsip.conf 2>/dev/null || echo "0") if [[ $device_count -gt 0 ]]; then echo "" echo -e "${YELLOW}Warning: This category has ${device_count} device(s)${NC}" echo "" echo " 1) Delete category only (reassign devices to 'uncategorized')" echo " 2) Delete category AND all devices in it" echo " 0) Cancel" read -p "Select: " del_choice case $del_choice in 1) # Ensure uncategorized category exists if ! grep -q "^uncategorized|" "$CATEGORIES_FILE" 2>/dev/null; then echo "uncategorized|Uncategorized|no|Default category for orphaned devices" >> "$CATEGORIES_FILE" fi # Reassign all devices to uncategorized backup_config "/etc/asterisk/pjsip.conf" sed -i "s/; === Device: \(.*\) (${cid})/; === Device: \1 (uncategorized)/" /etc/asterisk/pjsip.conf # Delete the category sed -i "/^${cid}|/d" "$CATEGORIES_FILE" print_success "Category deleted, ${device_count} device(s) moved to 'uncategorized'" rebuild_dialplan ;; 2) echo "" echo -e "${RED}WARNING: This will DELETE ${device_count} device(s)!${NC}" read -p "Type 'DELETE ALL' to confirm: " confirm if [[ "$confirm" == "DELETE ALL" ]]; then backup_config "/etc/asterisk/pjsip.conf" # Get all extensions in this category local exts_to_delete="" local in_device=0 local current_ext="" local current_cat="" while IFS= read -r line; do if [[ "$line" == *"; === Device:"* ]]; then local temp="${line#*; === Device: }" temp="${temp% ===}" [[ "$temp" == *"[AA:"* ]] && temp="${temp% \[AA:*\]}" current_cat="${temp##* (}"; current_cat="${current_cat%)}" fi if [[ "$line" =~ ^\[([0-9]+)\] ]]; then current_ext="${BASH_REMATCH[1]}" if [[ "$current_cat" == "$cid" ]]; then exts_to_delete="${exts_to_delete} ${current_ext}" fi fi done < /etc/asterisk/pjsip.conf # Delete all device sections for this category for ext in $exts_to_delete; do sed -i "/^; === Device:.*${ext}.* (${cid})/,/^$/d" /etc/asterisk/pjsip.conf sed -i "/^\[${ext}\]/,/^$/d" /etc/asterisk/pjsip.conf done # Delete the category sed -i "/^${cid}|/d" "$CATEGORIES_FILE" asterisk -rx "pjsip reload" 2>/dev/null rebuild_dialplan print_success "Category and ${device_count} device(s) deleted" else print_error "Cancelled" fi ;; 0) print_error "Cancelled" return ;; esac else # No devices, just delete the category sed -i "/^${cid}|/d" "$CATEGORIES_FILE" print_success "Category deleted (no devices affected)" rebuild_dialplan fi ;; esac } manage_rooms() { print_header "Manage Rooms" initialize_default_categories echo "Current Rooms:" local index=1 while IFS='|' read -r rext rname rmem rtime rtype; do [[ "$rext" =~ ^# ]] && continue [[ -z "$rext" ]] && continue local type_text="Ring Group" [[ "$rtype" == "page" ]] && type_text="${GREEN}PAGE/INTERCOM${NC}" echo -e " ${CYAN}$index)${NC} ${BOLD}$rname${NC} ($rext) - $type_text" echo -e " Members: $rmem" ((index++)) done < "$ROOMS_FILE" echo "" echo " 1) Add Room" echo " 2) Rename Room" echo " 3) Edit Room Members" echo " 4) Delete Room" echo " 0) Back" read -p "Select: " choice case $choice in 1) read -p "Room Extension: " new_ext read -p "Room Name: " new_name echo " 1) Ring Group (Phones ring)" echo " 2) Page/Intercom (Auto-answer)" read -p "Select [1]: " type_sel local rtype="ring" [[ "$type_sel" == "2" ]] && rtype="page" read -p "Members (e.g. 101,102): " members echo "${new_ext}|${new_name}|${members}|60|${rtype}" >> "$ROOMS_FILE" rebuild_dialplan print_success "Room Created" ;; 2) read -p "Select Room #: " rnum local target_line="" local count=0 while IFS= read -r line; do if [[ ! "$line" =~ ^# ]] && [[ -n "$line" ]]; then ((count++)) if [[ $count -eq $rnum ]]; then target_line="$line"; break; fi fi done < "$ROOMS_FILE" if [[ -n "$target_line" ]]; then IFS='|' read -r rext old_name rmem rtime rtype <<< "$target_line" echo "Current name: $old_name" read -p "New name: " new_name if [[ -z "$new_name" ]]; then print_error "Name cannot be empty" return fi backup_config "$ROOMS_FILE" sed -i "/^${rext}|/d" "$ROOMS_FILE" echo "${rext}|${new_name}|${rmem}|${rtime}|${rtype}" >> "$ROOMS_FILE" rebuild_dialplan print_success "Room renamed: ${old_name} → ${new_name}" else print_error "Invalid selection" fi ;; 3) read -p "Select Room #: " rnum local target_line="" local count=0 while IFS= read -r line; do if [[ ! "$line" =~ ^# ]] && [[ -n "$line" ]]; then ((count++)) if [[ $count -eq $rnum ]]; then target_line="$line"; break; fi fi done < "$ROOMS_FILE" if [[ -n "$target_line" ]]; then IFS='|' read -r rext rname rmem rtime rtype <<< "$target_line" echo "Current members: $rmem" read -p "New members: " new_mem sed -i "/^${rext}|/d" "$ROOMS_FILE" echo "${rext}|${rname}|${new_mem}|${rtime}|${rtype}" >> "$ROOMS_FILE" rebuild_dialplan print_success "Room Updated" fi ;; 4) read -p "Select Room #: " rnum local count=0 local target_ext="" local target_name="" while IFS='|' read -r rext rname rrest; do if [[ ! "$rext" =~ ^# ]] && [[ -n "$rext" ]]; then ((count++)) if [[ $count -eq $rnum ]]; then target_ext="$rext" target_name="$rname" break fi fi done < "$ROOMS_FILE" if [[ -n "$target_ext" ]]; then echo "" echo -e "${YELLOW}Note: Deleting a room only removes the group.${NC}" echo -e "${YELLOW}Individual devices in this room are NOT deleted.${NC}" echo "" read -p "Delete room '${target_name}' (${target_ext})? [y/N]: " confirm if [[ "$confirm" =~ ^[Yy]$ ]]; then sed -i "/^${target_ext}|/d" "$ROOMS_FILE" rebuild_dialplan print_success "Room deleted (devices unaffected)" else print_error "Cancelled" fi fi ;; esac } add_device_menu() { print_header "Add Device" load_config # Load saved configuration to check ENABLE_TLS, DOMAIN_NAME, etc. list_categories read -p "Category number: " cat_num local cat_data=$(get_category_by_index "$cat_num") if [[ -z "$cat_data" ]]; then print_error "Invalid"; return; fi local cat_id=$(echo "$cat_data" | cut -d'|' -f1) local cat_name=$(echo "$cat_data" | cut -d'|' -f2) local auto_answer=$(echo "$cat_data" | cut -d'|' -f3) local start_range=101 end_range=199 case "$cat_id" in kiosk) start_range=101; end_range=199 ;; mobile) start_range=201; end_range=299 ;; *) start_range=301; end_range=399 ;; esac local suggested_ext="" for ext in $(seq $start_range $end_range); do if ! grep -q "^\[${ext}\]" /etc/asterisk/pjsip.conf 2>/dev/null; then suggested_ext=$ext; break fi done read -p "Extension [$suggested_ext]: " ext ext="${ext:-$suggested_ext}" if grep -q "^\[${ext}\]" /etc/asterisk/pjsip.conf 2>/dev/null; then print_error "Extension exists!"; return fi read -p "Name: " name name="${name:-Device $ext}" local pass=$(generate_password) local override_tag="" if [[ "$auto_answer" == "no" ]]; then read -p "Force AUTO-ANSWER? [y/N]: " force_aa [[ "$force_aa" =~ ^[Yy]$ ]] && override_tag="[AA:yes]" && auto_answer="yes" elif [[ "$auto_answer" == "yes" ]]; then read -p "Force RING? [y/N]: " force_ring [[ "$force_ring" =~ ^[Yy]$ ]] && override_tag="[AA:no]" && auto_answer="no" fi # CONNECTION TYPE SELECTION local conn_type="lan" local transport_block="" local encryption_block="" local ice_block="" local display_server="" local display_port="5060" local display_transport="UDP" local display_encryption="None" echo "" echo "═══════════════════════════════════════════════════════════════" echo -e " HOW WILL THIS DEVICE CONNECT?" echo "═══════════════════════════════════════════════════════════════" echo "" echo -e " 1) ${GREEN}LAN/VPN${NC} - Same network or VPN tunnel (UDP)" if [[ "$ENABLE_TLS" == "y" && -n "$DOMAIN_NAME" ]]; then echo -e " 2) ${CYAN}FQDN${NC} - Internet or cross-VLAN via ${DOMAIN_NAME} (TLS)" else echo -e " 2) ${YELLOW}FQDN${NC} - Not configured (run 'Setup Internet Access' first)" fi echo "" read -p " Select [1]: " conn_choice conn_choice="${conn_choice:-1}" if [[ "$conn_choice" == "1" ]]; then # LAN/VPN - UDP, no encryption (explicit transport prevents TLS fallback) transport_block="transport=transport-udp" encryption_block="media_encryption=no" display_server="$(hostname -I | awk '{print $1}')" display_port="5060" display_transport="UDP" display_encryption="None" elif [[ "$conn_choice" == "2" ]]; then if [[ "$ENABLE_TLS" != "y" || -z "$DOMAIN_NAME" ]]; then print_error "FQDN access not configured. Run 'Setup Internet Access' first." return fi conn_type="fqdn" transport_block="transport=transport-tls" encryption_block="media_encryption=sdes" ice_block="ice_support=yes" display_server="$DOMAIN_NAME" display_port="5061" display_transport="TLS" display_encryption="SRTP (SDES)" fi backup_config "/etc/asterisk/pjsip.conf" cat >> /etc/asterisk/pjsip.conf << EOF ; === Device: $name ($cat_id) $override_tag === [${ext}] type=endpoint context=intercom ${transport_block} disallow=all allow=opus allow=ulaw allow=alaw allow=g722 ${encryption_block} direct_media=no rtp_symmetric=yes force_rport=yes rewrite_contact=yes ${ice_block} auth=${ext} aors=${ext} callerid="${name}" <${ext}> [${ext}] type=auth auth_type=userpass username=${ext} password=${pass} [${ext}] type=aor max_contacts=5 remove_existing=yes qualify_frequency=60 EOF chown -R asterisk:asterisk /etc/asterisk 2>/dev/null || true asterisk -rx "pjsip reload" >/dev/null 2>&1 rebuild_dialplan # Prepare provisioning URLs if HTTP server is configured local server_ip=$(hostname -I | awk '{print $1}') local prov_url_http="" local prov_url_https="" if [[ -f /etc/asterisk/http.conf ]] && grep -q "enabled=yes" /etc/asterisk/http.conf 2>/dev/null; then prov_url_http="http://${server_ip}:8088/static/linphone.xml" if [[ -n "$DOMAIN_NAME" ]]; then prov_url_https="https://${DOMAIN_NAME}:8089/static/linphone.xml" fi fi echo "" echo "═══════════════════════════════════════════════════════════════" echo " DEVICE ADDED: $name (Extension $ext)" echo "═══════════════════════════════════════════════════════════════" echo "" echo -e " ${BOLD}Server Details:${NC}" echo " Server: ${display_server}" echo " Port: ${display_port}" echo " Transport: ${display_transport}" echo " Extension: $ext" echo " Password: $pass" echo " Encryption: ${display_encryption}" echo "" echo "═══════════════════════════════════════════════════════════════" echo -e " ${BOLD}LINPHONE SETUP${NC}" echo "═══════════════════════════════════════════════════════════════" echo "" if [[ -n "$prov_url_http" ]]; then echo " Remote Provisioning (Recommended):" echo " 1. In Linphone → Settings → Remote provisioning" echo " 2. Enter URL:" echo " ${prov_url_http}" [[ -n "$prov_url_https" ]] && echo " OR ${prov_url_https}" echo " 3. Tap 'Fetch' to apply configuration" echo "" echo " OR Manual Setup:" else echo " Manual Setup:" fi echo " 1. Add Account → Use SIP account" echo " 2. Username: $ext" echo " 3. Password: $pass" echo " 4. Domain: ${display_server}" echo " 5. Transport: ${display_transport}" echo "" echo "═══════════════════════════════════════════════════════════════" echo -e " ${BOLD}BARESIP SETUP (if Linphone has audio issues)${NC}" echo "═══════════════════════════════════════════════════════════════" echo "" echo " Baresip often works better on privacy-focused Android ROMs." echo " Two-step manual configuration required:" echo "" echo " Step 1: Add Account" echo " Menu (☰) → Accounts → Add (+)" echo " SIP URI: ${ext}@${display_server}" echo " Save (✓)" echo "" echo " Step 2: Edit Account (Complete Config)" echo " Tap account → Edit" echo " Auth Username: $ext (JUST the number!)" echo " Auth Password: $pass" echo " Outbound Proxy: ${display_server} (JUST the domain!)" echo " Media Encryption: srtp (select from dropdown)" echo " Register: ✓ (check box)" echo " Save (✓)" echo "" echo " Verify: Look for green dot or 'Registered' status" echo " To call: Just dial extension (101, 202, etc.)" echo "" echo " For detailed Baresip instructions:" echo " Server Settings → Provisioning Manager → Create Baresip Config" echo "" echo "═══════════════════════════════════════════════════════════════" echo "" echo " NOTE: These instructions work for most SIP apps (Zoiper," echo " sipnetic, etc.) - just use the same credentials." echo "" echo "═══════════════════════════════════════════════════════════════" } remove_device() { print_header "Remove Device" declare -A REMOVE_MAP declare -A NAME_MAP local count=1 local current_name="" echo "Select device to remove:" echo "" while IFS= read -r line; do if [[ "$line" == *"; === Device:"* ]]; then local temp="${line#*; === Device: }" temp="${temp% ===}" temp="${temp% \[AA:*\]}" current_name="${temp% (*)}" fi if [[ "$line" =~ ^\[([0-9]+)\]$ && "$current_name" != "" ]]; then local ext="${BASH_REMATCH[1]}" echo " ${count}) Ext ${ext} - ${current_name}" REMOVE_MAP[$count]=$ext NAME_MAP[$count]="$current_name" ((count++)) current_name="" fi done < /etc/asterisk/pjsip.conf echo "" echo " 98) DELETE ALL DEVICES" echo " 0) Cancel" echo "" read -p "Select: " choice if [[ "$choice" == "98" ]]; then echo "" print_warn "This will DELETE ALL DEVICES!" read -p "Type 'DELETE ALL' to confirm: " confirm if [[ "$confirm" == "DELETE ALL" ]]; then backup_config "/etc/asterisk/pjsip.conf" # Remove all device sections - use awk to properly handle all sections awk ' /^; === Device:/ { skip = 1; next } /^\[[0-9]{3}\]$/ { if (skip) next } /^type=(endpoint|auth|aor)/ { if (skip) next } /^$/ { if (skip) { skip = 0; next } } !skip { print } ' /etc/asterisk/pjsip.conf > /etc/asterisk/pjsip.conf.tmp mv /etc/asterisk/pjsip.conf.tmp /etc/asterisk/pjsip.conf chown asterisk:asterisk /etc/asterisk/pjsip.conf asterisk -rx "pjsip reload" 2>/dev/null rebuild_dialplan print_success "All devices deleted" else print_error "Cancelled" fi return fi [[ "$choice" == "0" || -z "${REMOVE_MAP[$choice]}" ]] && return local ext="${REMOVE_MAP[$choice]}" local name="${NAME_MAP[$choice]}" read -p "Confirm removal of $ext ($name)? [y/N]: " confirm if [[ "$confirm" =~ ^[Yy]$ ]]; then backup_config "/etc/asterisk/pjsip.conf" # Use awk to remove the device comment and ALL three sections for this extension awk -v ext="$ext" ' BEGIN { skip = 0; found_ext = 0 } # Match device comment line - start potential skip /^; === Device:/ { pending_comment = $0; next } # Check if this is the extension we want to delete $0 ~ "^\\[" ext "\\]$" { if (pending_comment != "") { # This is our device - skip the comment and this section skip = 1 found_ext = 1 pending_comment = "" next } else if (found_ext) { # Additional sections for same extension (auth, aor) skip = 1 next } } # If we have a pending comment for a different extension, print it pending_comment != "" && $0 !~ "^\\[" ext "\\]$" { print pending_comment pending_comment = "" } # Skip lines until empty line skip && /^$/ { skip = 0; next } skip { next } { print } ' /etc/asterisk/pjsip.conf > /etc/asterisk/pjsip.conf.tmp mv /etc/asterisk/pjsip.conf.tmp /etc/asterisk/pjsip.conf chown asterisk:asterisk /etc/asterisk/pjsip.conf asterisk -rx "pjsip reload" 2>/dev/null rebuild_dialplan print_success "Removed extension $ext ($name)" fi } rename_device() { print_header "Rename Device" declare -A DEVICE_MAP declare -A NAME_MAP declare -A AA_MAP local count=1 echo "Select device to rename:" echo "" while IFS= read -r line; do if [[ "$line" == *"; === Device:"* ]]; then local temp="${line#*; === Device: }" temp="${temp% ===}" local aa_tag="" if [[ "$temp" == *"[AA:yes]"* ]]; then aa_tag="[AA:yes]" temp="${temp% \[AA:yes\]}" elif [[ "$temp" == *"[AA:no]"* ]]; then aa_tag="[AA:no]" temp="${temp% \[AA:no\]}" fi local name="${temp% (*)}" local cat="${temp##* (}"; cat="${cat%)}" fi if [[ "$line" =~ ^\[([0-9]+)\]$ && -n "$name" ]]; then local ext="${BASH_REMATCH[1]}" echo " ${count}) Ext ${ext} - ${name} (${cat})" DEVICE_MAP[$count]=$ext NAME_MAP[$count]="${name}|${cat}" AA_MAP[$count]="${aa_tag}" ((count++)) name="" fi done < /etc/asterisk/pjsip.conf echo "" echo " 0) Cancel" echo "" read -p "Select: " choice [[ "$choice" == "0" || -z "${DEVICE_MAP[$choice]}" ]] && return local ext="${DEVICE_MAP[$choice]}" local info="${NAME_MAP[$choice]}" local aa_tag="${AA_MAP[$choice]}" local old_name="${info%|*}" local cat="${info##*|}" echo "" echo "Current name: ${old_name}" read -p "New name: " new_name if [[ -z "$new_name" ]]; then print_error "Name cannot be empty" return fi # Backup config backup_config "/etc/asterisk/pjsip.conf" # Use awk to properly update both the comment line (preserving AA tag) and callerid awk -v ext="$ext" -v old_name="$old_name" -v new_name="$new_name" -v cat="$cat" -v aa_tag="$aa_tag" ' # Update device comment line /^; === Device:/ && $0 ~ old_name && $0 ~ cat { if (aa_tag != "") { print "; === Device: " new_name " (" cat ") " aa_tag " ===" } else { print "; === Device: " new_name " (" cat ") ===" } next } # Track when we are in the correct extension section $0 ~ "^\\[" ext "\\]$" { in_ext = 1 } /^$/ { in_ext = 0 } # Update callerid in the extension section in_ext && /^callerid=/ { print "callerid=\"" new_name "\" <" ext ">" next } { print } ' /etc/asterisk/pjsip.conf > /etc/asterisk/pjsip.conf.tmp mv /etc/asterisk/pjsip.conf.tmp /etc/asterisk/pjsip.conf chown asterisk:asterisk /etc/asterisk/pjsip.conf # Reload Asterisk asterisk -rx "pjsip reload" 2>/dev/null rebuild_dialplan quiet print_success "Device renamed: ${old_name} → ${new_name}" } show_registered_devices() { # Collect all device data declare -A device_data local dev_name="" dev_cat="" while IFS= read -r line; do if [[ "$line" == *"; === Device:"* ]]; then # Remove the prefix and suffix, handling variable whitespace local temp="${line#*; === Device: }" temp="${temp%% ===}" # Use %% to handle multiple spaces before === temp="${temp## }" # Trim leading spaces temp="${temp%% }" # Trim trailing spaces [[ "$temp" == *"[AA:"* ]] && temp="${temp% \[AA:*\]}" # Extract category - everything inside the last (...) dev_cat="${temp##*\(}" dev_cat="${dev_cat%\)}" dev_cat="${dev_cat## }" # Trim any leading spaces dev_cat="${dev_cat%% }" # Trim any trailing spaces # Extract name - everything before the last ( dev_name="${temp%% \(*}" fi if [[ "$line" =~ ^\[([0-9]+)\] ]]; then local ext="${BASH_REMATCH[1]}" if [[ -n "$dev_name" ]]; then device_data[$ext]="${dev_name}|${dev_cat}" dev_name="" dev_cat="" fi fi done < /etc/asterisk/pjsip.conf # Interactive loop while true; do # Group by category declare -A categories declare -A category_names for ext in "${!device_data[@]}"; do local info="${device_data[$ext]}" local cat="${info##*|}" categories[$cat]="${categories[$cat]} $ext" done # Get full category names from categories file while IFS='|' read -r cat_id cat_name auto_answer description; do [[ "$cat_id" =~ ^# ]] && continue [[ -z "$cat_id" ]] && continue category_names[$cat_id]="$cat_name" done < "$CATEGORIES_FILE" clear print_header "Device Status" echo "Select category to view:" echo " 1) All devices" local i=2 declare -A cat_menu for cat in $(echo "${!categories[@]}" | tr ' ' '\n' | sort); do local display_name="${category_names[$cat]:-$cat}" echo " ${i}) ${display_name}" cat_menu[$i]="$cat" ((i++)) done echo " 0) Back to menu" echo "" read -p "Select [1]: " cat_choice [[ "$cat_choice" == "0" ]] && return cat_choice="${cat_choice:-1}" clear print_header "Device Status" printf "${CYAN}%-6s %-25s %-15s %-15s %-15s${NC}\n" "Ext" "Name" "Category" "Status" "Password" echo "--------------------------------------------------------------------------------------------" if [[ "$cat_choice" == "1" ]]; then # Show all devices for ext in $(echo "${!device_data[@]}" | tr ' ' '\n' | sort -n); do local info="${device_data[$ext]}" local name="${info%|*}" local cat="${info##*|}" local cat_display="${category_names[$cat]:-$cat}" local status="${RED}Offline${NC}" local avail=$(asterisk -rx "pjsip show endpoint ${ext}" 2>/dev/null | grep -E "Contact:.*(Avail|NonQual)" || true) [[ -n "$avail" ]] && status="${GREEN}Online${NC}" local password=$(grep -A 10 "^\[$ext\]" /etc/asterisk/pjsip.conf | grep "password=" | head -1 | cut -d= -f2) printf "%-6s %-25s %-15s %b %-15s\n" "$ext" "${name:0:23}" "${cat_display:0:13}" "$status" "$password" done else # Show specific category local selected_cat="${cat_menu[$cat_choice]}" if [[ -n "$selected_cat" ]]; then local cat_display="${category_names[$selected_cat]:-$selected_cat}" echo -e "${BOLD}Showing: ${cat_display}${NC}" echo "" for ext in $(echo "${categories[$selected_cat]}" | tr ' ' '\n' | sort -n); do local info="${device_data[$ext]}" local name="${info%|*}" local cat="${info##*|}" local cat_display="${category_names[$cat]:-$cat}" local status="${RED}Offline${NC}" local avail=$(asterisk -rx "pjsip show endpoint ${ext}" 2>/dev/null | grep -E "Contact:.*(Avail|NonQual)" || true) [[ -n "$avail" ]] && status="${GREEN}Online${NC}" local password=$(grep -A 10 "^\[$ext\]" /etc/asterisk/pjsip.conf | grep "password=" | head -1 | cut -d= -f2) printf "%-6s %-25s %-15s %b %-15s\n" "$ext" "${name:0:23}" "${cat_display:0:13}" "$status" "$password" done fi fi echo "" echo "Connection Details:" echo " Domain: ${DOMAIN_NAME:-$(hostname -I | awk '{print $1}')}" echo " Port: ${DEFAULT_SIP_PORT}/udp (LAN) or ${DEFAULT_SIPS_PORT}/tcp (TLS)" echo "" read -p "Press Enter to select another category (or 0 to exit)... " done } # ================================================================ # 4. PTT WIZARD (Fixed: Mute by default) # ================================================================ configure_ptt_menu() { print_header "Configure PTT Button" detect_ptt_button } detect_ptt_button() { # Ensure evtest is installed if ! command -v evtest &>/dev/null; then apt install -y evtest >/dev/null 2>&1 fi # Add user to input group [[ -n "$KIOSK_USER" ]] && usermod -aG input "$KIOSK_USER" 2>/dev/null || true print_info "Scanning input devices..." echo "" declare -a SUGGESTED_DEVICES SUGGESTED_NAMES OTHER_DEVICES OTHER_NAMES for dev in /dev/input/event*; do [[ -e "$dev" ]] || continue local name=$(cat "/sys/class/input/$(basename $dev)/device/name" 2>/dev/null || echo "Unknown") local lname=$(echo "$name" | tr '[:upper:]' '[:lower:]') # Filter out system devices that aren't PTT candidates if [[ "$lname" =~ (power.button|sleep.button|lid.switch|virtual|video.bus|hdmi|dp,pcm|hotkey|touchpad|touchscreen) ]]; then OTHER_DEVICES+=("$dev") OTHER_NAMES+=("$name") # Prioritize keyboards, USB HID devices, pedals elif [[ "$lname" =~ (keyboard|sayo.*nano$|pedal|foot|^hid) ]]; then SUGGESTED_DEVICES+=("$dev") SUGGESTED_NAMES+=("$name") else OTHER_DEVICES+=("$dev") OTHER_NAMES+=("$name") fi done # Display suggested devices first if [[ ${#SUGGESTED_DEVICES[@]} -gt 0 ]]; then echo -e "${GREEN}Keyboards and USB buttons:${NC}" for i in "${!SUGGESTED_DEVICES[@]}"; do printf " ${CYAN}%2d)${NC} %s - %s\n" "$((i+1))" "$(basename ${SUGGESTED_DEVICES[$i]})" "${SUGGESTED_NAMES[$i]}" done echo "" fi # Display other devices if [[ ${#OTHER_DEVICES[@]} -gt 0 ]]; then echo -e "${YELLOW}Other devices:${NC}" local offset=${#SUGGESTED_DEVICES[@]} for i in "${!OTHER_DEVICES[@]}"; do printf " ${CYAN}%2d)${NC} %s - %s\n" "$((offset+i+1))" "$(basename ${OTHER_DEVICES[$i]})" "${OTHER_NAMES[$i]}" done echo "" fi local ALL_DEVICES=("${SUGGESTED_DEVICES[@]}" "${OTHER_DEVICES[@]}") local total=${#ALL_DEVICES[@]} if [[ $total -eq 0 ]]; then print_error "No input devices found" return 1 fi echo " 0) Back" echo "" read -p "Select device [1]: " selection selection="${selection:-1}" [[ "$selection" == "0" ]] && return 0 [[ "$selection" -lt 1 || "$selection" -gt "$total" ]] && { print_error "Invalid selection"; return 1; } PTT_DEVICE="${ALL_DEVICES[$((selection-1))]}" local dev_name=$(cat "/sys/class/input/$(basename $PTT_DEVICE)/device/name" 2>/dev/null || echo "Unknown") echo "" print_success "Selected: $dev_name" echo " ($PTT_DEVICE)" echo "" # Key detection loop while true; do echo -e "${YELLOW}══════════════════════════════════════════════════${NC}" echo -e "${YELLOW} DO NOT PRESS YET - wait for countdown${NC}" echo -e "${YELLOW}══════════════════════════════════════════════════${NC}" for i in 5 4 3 2 1; do echo -ne "\r Waiting... $i " sleep 1 done echo "" echo "" echo -e "${GREEN}>>> NOW PRESS YOUR PTT BUTTON <<<${NC}" echo "" local detected_code=$(timeout 10 evtest "$PTT_DEVICE" 2>/dev/null | grep -m1 "value 1$" | grep -oP 'code \K[0-9]+' || echo "") if [[ -n "$detected_code" && "$detected_code" -gt 0 ]]; then # Map common key codes to friendly names local key_name="Key $detected_code" case "$detected_code" in 1) key_name="Escape" ;; 28) key_name="Enter" ;; 57) key_name="Spacebar" ;; 69) key_name="Num Lock" ;; 113) key_name="Mute" ;; 114) key_name="Volume Down" ;; 115) key_name="Volume Up" ;; 116) key_name="Power" ;; 142) key_name="Sleep" ;; 272) key_name="Left Click" ;; 273) key_name="Right Click" ;; esac print_success "Detected: $key_name (code $detected_code)" echo "" read -p "Use this key? [Y/n]: " use_key if [[ ! "$use_key" =~ ^[Nn]$ ]]; then PTT_KEYCODE="$detected_code" PTT_KEYNAME="$key_name" break fi else print_warn "No button press detected" fi echo "" echo " 1) Try again" echo " 2) Enter key code manually" echo " 3) Cancel" read -p "Select [1]: " retry case "${retry:-1}" in 2) read -p "Enter key code: " PTT_KEYCODE PTT_KEYNAME="Manual" break ;; 3) return 1 ;; esac done # Ensure user is in input group (critical for PTT device access) if [[ -n "$KIOSK_USER" ]]; then if ! id -nG "$KIOSK_USER" | grep -qw "input"; then print_info "Adding $KIOSK_USER to input group..." usermod -aG input "$KIOSK_USER" echo "" print_error "IMPORTANT: User added to 'input' group" echo " User must log out and log back in (or reboot) for group change to take effect." echo " PTT will NOT work until then!" echo "" read -p "Press Enter to acknowledge..." fi fi # Save configuration via save_config (will set proper permissions) save_config print_success "PTT configured: $PTT_KEYNAME on $(basename $PTT_DEVICE)" echo "" echo "═══════════════════════════════════════════════════════" echo " PTT Configuration Complete" echo "═══════════════════════════════════════════════════════" echo " Device: $PTT_DEVICE" echo " Button: $PTT_KEYNAME" echo " User: ${KIOSK_USER:-not set}" echo "" echo " Testing PTT:" echo " 1. Check logs: journalctl -t kiosk-ptt -f" echo " 2. Press PTT button" echo " 3. You should see: 'PTT pressed - mic unmuted'" echo "" echo " If you see 'Permission denied' errors:" echo " - User needs to be in 'input' group (already added above)" echo " - Log out and log back in, or reboot" echo "═══════════════════════════════════════════════════════" # Restart PTT service if client is installed if [[ "$INSTALLED_CLIENT" == "y" && -n "$KIOSK_USER" ]]; then local user_dbus="XDG_RUNTIME_DIR=/run/user/${KIOSK_UID}" echo "" print_info "Restarting PTT service..." sudo -u "$KIOSK_USER" $user_dbus systemctl --user daemon-reload 2>/dev/null sudo -u "$KIOSK_USER" $user_dbus systemctl --user restart kiosk-ptt 2>/dev/null || true sleep 2 echo "" echo "Checking PTT status..." journalctl -t kiosk-ptt -n 5 --no-pager 2>/dev/null || echo " No logs yet (check after logging out/in if needed)" fi return 0 } create_ptt_handler() { cat > /usr/local/bin/kiosk-ptt << 'PTTSCRIPT' #!/bin/bash CONFIG="/etc/easy-asterisk/config" PTT_CONFIG="/etc/easy-asterisk/ptt-device" [[ -f "$CONFIG" ]] && source "$CONFIG" [[ -f "$PTT_CONFIG" ]] && source "$PTT_CONFIG" # Exit if no PTT device configured - leave audio unmuted for normal kiosk operation [[ -z "$PTT_DEVICE" ]] && exit 0 # Ensure we have the user's runtime directory if [[ -z "$XDG_RUNTIME_DIR" ]]; then # If running as systemd service, this should already be set # But if not, try to detect it if [[ -n "$KIOSK_UID" ]]; then export XDG_RUNTIME_DIR="/run/user/${KIOSK_UID}" else # Fall back to current user export XDG_RUNTIME_DIR="/run/user/$(id -u)" fi fi # Wait for PipeWire/PulseAudio to be ready for i in {1..10}; do if pactl info >/dev/null 2>&1; then break fi sleep 1 done # PTT mode: Mute audio source on start, unmute only when button pressed pactl set-source-mute @DEFAULT_SOURCE@ 1 2>/dev/null || { logger -t kiosk-ptt "ERROR: Failed to mute audio source" exit 1 } logger -t kiosk-ptt "PTT handler started, microphone muted, listening on $PTT_DEVICE" # Unmute on press, mute on release evtest --grab "$PTT_DEVICE" 2>/dev/null | while read -r line; do if [[ "$line" =~ "value 1" ]]; then pactl set-source-mute @DEFAULT_SOURCE@ 0 2>/dev/null logger -t kiosk-ptt "PTT pressed - mic unmuted" fi if [[ "$line" =~ "value 0" ]]; then pactl set-source-mute @DEFAULT_SOURCE@ 1 2>/dev/null logger -t kiosk-ptt "PTT released - mic muted" fi done PTTSCRIPT chmod +x /usr/local/bin/kiosk-ptt } # ================================================================ # 5. AUDIO DUCKING # ================================================================ configure_audio_ducking() { [[ -z "$KIOSK_USER" ]] && return local wp_dir="/home/${KIOSK_USER}/.config/wireplumber/wireplumber.conf.d" mkdir -p "$wp_dir" cat > "${wp_dir}/50-intercom-ducking.conf" << 'EOF' wireplumber.settings = { linking.allow-moving-streams = true } EOF chown -R ${KIOSK_USER}:${KIOSK_USER} "/home/${KIOSK_USER}/.config" } ensure_audio_unmuted() { [[ -z "$KIOSK_USER" ]] && return [[ -z "$KIOSK_UID" ]] && return # Only unmute if PTT is not configured if [[ ! -f /etc/easy-asterisk/ptt-device ]]; then local user_dbus="XDG_RUNTIME_DIR=/run/user/${KIOSK_UID}" # Wait a moment for PipeWire to initialize sleep 2 # Unmute all sources and sinks sudo -u "$KIOSK_USER" $user_dbus pactl set-source-mute @DEFAULT_SOURCE@ 0 2>/dev/null || true sudo -u "$KIOSK_USER" $user_dbus pactl set-sink-mute @DEFAULT_SINK@ 0 2>/dev/null || true # Set reasonable volume levels if they're at 0 local source_vol=$(sudo -u "$KIOSK_USER" $user_dbus pactl get-source-volume @DEFAULT_SOURCE@ 2>/dev/null | grep -oP '\d+%' | head -1 | tr -d '%') local sink_vol=$(sudo -u "$KIOSK_USER" $user_dbus pactl get-sink-volume @DEFAULT_SINK@ 2>/dev/null | grep -oP '\d+%' | head -1 | tr -d '%') [[ -n "$source_vol" && "$source_vol" -lt 50 ]] && sudo -u "$KIOSK_USER" $user_dbus pactl set-source-volume @DEFAULT_SOURCE@ 75% 2>/dev/null || true [[ -n "$sink_vol" && "$sink_vol" -lt 50 ]] && sudo -u "$KIOSK_USER" $user_dbus pactl set-sink-volume @DEFAULT_SINK@ 75% 2>/dev/null || true fi } # ================================================================ # 6. DIAGNOSTICS & FIREWALL # ================================================================ show_port_requirements() { print_header "Port / Firewall Requirements" echo "This server needs traffic to pass from your Clients (Kiosks/Phones)." echo "" echo "Does your Asterisk server have a PUBLIC IP (VPS/Cloud)?" echo " -> YES: You must use 'Forwarding' (DNAT) rules on your router." echo " -> NO: You must use 'Allow/Pass' rules on your VLAN interfaces." echo "" echo "Required Ports:" echo "┌──────────────────┬──────────┬───────────────────────────────┐" echo "│ Port │ Protocol │ Purpose │" echo "├──────────────────┼──────────┼───────────────────────────────┤" echo "│ 5060 │ UDP │ SIP Signaling (Registration) │" echo "│ 5061 │ TCP │ SIP-TLS Signaling (Secure) │" echo "│ 10000-20000 │ UDP │ RTP Media (Audio/Video) │" if [[ "$USE_COTURN" == "y" ]]; then echo "│ ${DEFAULT_TURN_PORT} │ UDP/TCP │ TURN Signaling (Handshake) │" echo "│ 49152-65535 │ UDP │ TURN Relay (Actual Media Path)│" fi echo "└──────────────────┴──────────┴───────────────────────────────┘" echo "" echo "NOTE: VPN Users" echo "If ALL clients and server are on a VPN (Tailscale/Wireguard), you DO NOT" echo "need port forwarding or COTURN. Just bind Asterisk to the VPN IP." } show_firewall_guide() { print_header "Interactive Firewall Guide (Hand-holding Mode)" echo "For: Routers with VLAN support" echo "" echo "=== SCENARIO A: INTERNAL ONLY (VLAN to VLAN) ===" echo "Example: Kiosks on VLAN 10, Server on VLAN 20" echo "GOAL: Allow Kiosks to talk to Server." echo "" echo "STEP 1: Log in to Router. Go to Firewall > Rules > VLAN 10 Interface." echo " (Do NOT use 'Port Forwarding' for internal VLANs!)" echo "" echo "STEP 2: Create Rule 1 (Signaling)" echo " - Action: Pass (Allow)" echo " - Protocol: UDP/TCP" echo " - Source: VLAN 10 Net" echo " - Dest: ${CURRENT_PUBLIC_IP:-Server_IP}" echo " - Port: 3478" echo "" echo "STEP 3: Create Rule 2 (The Relay Range - CRITICAL)" echo " - Action: Pass (Allow)" echo " - Protocol: UDP" echo " - Source: VLAN 10 Net" echo " - Dest: ${CURRENT_PUBLIC_IP:-Server_IP}" echo " - Port Range:" echo " From: 49152" echo " To: 65535" echo " (Note: Type these numbers in the Start/End boxes)" echo "" echo "================================================" echo "" echo "=== SCENARIO B: EXTERNAL ACCESS (Internet to LAN) ===" echo "Example: Remote phone connecting from a hotel." echo "GOAL: Forward traffic from Internet to Server." echo "" echo "STEP 1: Go to Firewall > NAT > Port Forwarding." echo "STEP 2: Create Rule." echo " - Interface: WAN" echo " - Protocol: UDP" echo " - Dest. Port: 3478 (and 49152-65535)" echo " - Redirect IP: ${CURRENT_PUBLIC_IP:-Server_IP}" echo "" read -p "Press Enter to return..." } show_preflight_check() { print_header "Pre-Flight Requirements Check" echo "Modern browsers (Chrome, Safari, Kiosk Mode) have strict security settings." echo "" echo "1. HTTPS / SSL Certificate (Required for Camera/Mic)" echo " - Browsers block Mic/Cam on 'Insecure Origins' (HTTP)." echo " - Exception: http://localhost is allowed." echo " - Solution: You NEED a domain (FQDN) and SSL Cert (LetsEncrypt)." echo " - Workaround: Use the 'Caddy Cert Sync' option in this script." echo "" echo "2. Static vs Dynamic IP" echo " - If your Public IP changes, COTURN will break." echo " - Solution: Use the 'Update IP manually' or auto-script in the menu." echo "" echo "3. VPN Alternative" echo " - A VPN (Tailscale) negates the need for COTURN and Port Forwarding." echo " - It treats all devices as if they are on the same flat network." echo "" read -p "Press Enter to return..." } test_sip_connectivity() { print_header "SIP Connectivity Test" if systemctl is-active asterisk >/dev/null; then print_success "Asterisk Running" else print_error "Asterisk Down" fi echo "" echo "Listening ports:" ss -ulnp | grep 5060 || echo " UDP 5060: Not listening" ss -tlnp | grep 5061 || echo " TCP 5061: Not listening" if [[ -n "$DOMAIN_NAME" ]]; then echo "" echo "TLS Certificate check:" timeout 5 openssl s_client -connect localhost:5061 -servername "$DOMAIN_NAME" 2>/dev/null | grep "Verify return code" || echo " TLS test failed" fi } verify_cidr_config() { print_header "CIDR Configuration" local my_ip=$(hostname -I | cut -d' ' -f1) echo "Server IP: $my_ip" echo "" echo "Current NAT settings in pjsip.conf:" grep -E "external_|local_net" /etc/asterisk/pjsip.conf 2>/dev/null || echo " No NAT settings found" } configure_vlan_subnets() { print_header "VLAN Configuration" load_config echo "VLAN Support for Asterisk Easy" echo "================================================" echo "" echo "If your network uses VLANs (Virtual LANs), you need to" echo "tell Asterisk about all the local subnets to prevent" echo "calls from dropping after 30 seconds." echo "" echo "Example subnets:" echo " 192.168.1.0/24 - Main network" echo " 192.168.10.0/24 - IoT VLAN" echo " 192.168.20.0/24 - Guest VLAN" echo " 10.0.0.0/8 - Large private network" echo "" read -p "Does your network use VLANs? (y/n) [${HAS_VLANS}]: " has_vlans has_vlans=${has_vlans:-$HAS_VLANS} if [[ "$has_vlans" =~ ^[Yy] ]]; then HAS_VLANS="y" echo "" echo "Current VLAN Subnets: ${VLAN_SUBNETS:-none}" echo "" echo "Enter VLAN subnets in CIDR notation, separated by spaces." echo "Example: 192.168.1.0/24 192.168.10.0/24 192.168.20.0/24" echo "" read -p "VLAN Subnets: " vlan_input if [[ -n "$vlan_input" ]]; then VLAN_SUBNETS="$vlan_input" save_config print_success "VLAN configuration saved" echo "" echo "Rebuilding pjsip.conf to apply changes..." generate_pjsip_conf asterisk -rx "module reload res_pjsip.so" 2>/dev/null print_success "Asterisk configuration updated" echo "" echo "═══════════════════════════════════════════════════════════" echo " VLAN DNS SETUP GUIDE (Split-Horizon)" echo "═══════════════════════════════════════════════════════════" echo "" echo "For proper VLAN operation with FQDNs, you need split-horizon DNS." echo "" read -p "Display DNS setup guide? (y/n) [y]: " show_dns show_dns=${show_dns:-y} if [[ "$show_dns" =~ ^[Yy]$ ]]; then cat << 'DNSGUIDE' WHAT YOU'RE ACHIEVING: • Devices on VLANs use router for DNS (ctrld) • ctrld split-horizon rules send FQDNs to the right LAN servers • Only ctrld (router) can talk to servers' DNS (protected by UFW) • No inter-VLAN routing is opened, just DNS and service ports 1. CTRLD.TOML (on OPNSense/Router): [listener.0] ip = '0.0.0.0' port = 53 [listener.0.policy] networks = [ { 'network.0' = ['upstream.0'] }, { 'network.1' = ['upstream.1'] } ] rules = [ { 'asterisk.mydomain.com' = ['upstream.4'] } ] [network.0] cidrs = ['192.168.1.0/24'] [network.1] cidrs = ['192.168.200.0/24'] [upstream.0] type = 'doh' endpoint = 'https://dns.controld.com/your-profile' timeout = 5000 [upstream.4] type = 'legacy' endpoint = '192.168.1.11' # This Asterisk server timeout = 3000 2. DNSMASQ ON THIS SERVER: sudo apt-get install dnsmasq echo "listen-address=127.0.0.1" >> /etc/dnsmasq.conf echo "listen-address=$(hostname -I | cut -d' ' -f1)" >> /etc/dnsmasq.conf echo "bind-interfaces" >> /etc/dnsmasq.conf echo "address=/asterisk.mydomain.com/$(hostname -I | cut -d' ' -f1)" >> /etc/dnsmasq.conf sudo systemctl restart dnsmasq 3. UFW RULES ON THIS SERVER: sudo ufw allow from 192.168.1.1 to any port 53 proto udp sudo ufw allow from 192.168.1.1 to any port 53 proto tcp sudo ufw deny 53 sudo ufw reload Replace 192.168.1.1 with your router's LAN IP. 4. OPNSENSE FIREWALL RULES (for each VLAN): Rule 1 - Allow DNS from VLAN to Router: Action: Pass Source: VLANxx net Destination: This Firewall Port: 53 (DNS) Protocol: TCP/UDP Rule 2 - Allow SIP/RTP from VLAN to Asterisk: Source: VLANxx net Destination: $(hostname -I | cut -d' ' -f1) Ports: 5060/udp, 5061/tcp, 10000-20000/udp 5. DHCP SETTINGS (OPNSense): For each VLAN, set DNS Servers to ONLY the router's VLAN IP. Do NOT enter this server's IP as DNS. ═══════════════════════════════════════════════════════════ DNSGUIDE fi else print_error "No subnets provided" fi else HAS_VLANS="n" VLAN_SUBNETS="" save_config print_success "VLAN support disabled" fi } # ================================================================ # PROVISIONING MANAGER # ================================================================ setup_http_provisioning() { print_header "HTTP Provisioning Setup" echo "This will configure Asterisk's built-in HTTP server for" echo "client provisioning (Linphone, etc.)." echo "" echo "Ports:" echo " HTTP: 8088" echo " HTTPS: 8089" echo "" # Create http.conf backup_config "/etc/asterisk/http.conf" 2>/dev/null cat > /etc/asterisk/http.conf << 'EOF' [general] enabled=yes bindaddr=0.0.0.0 bindport=8088 tlsenable=yes tlsbindaddr=0.0.0.0:8089 tlscertfile=/etc/asterisk/certs/server.crt tlsprivatekey=/etc/asterisk/certs/server.key ; Serve static files from /var/lib/asterisk/static-http enablestatic=yes redirect=/static /var/lib/asterisk/static-http ; Security session_limit=100 session_inactivity=30000 session_keep_alive=15000 EOF chown asterisk:asterisk /etc/asterisk/http.conf # Create provisioning directory mkdir -p "$PROVISIONING_DIR" chown asterisk:asterisk "$PROVISIONING_DIR" # Create symlink if needed (Ubuntu/Debian fix) if [[ ! -L /usr/share/asterisk/static-http ]]; then mkdir -p /usr/share/asterisk ln -sf "$PROVISIONING_DIR" /usr/share/asterisk/static-http print_info "Created symlink: /usr/share/asterisk/static-http -> $PROVISIONING_DIR" fi # Reload Asterisk HTTP module asterisk -rx "module reload res_http_post.so" 2>/dev/null || true asterisk -rx "http show status" 2>/dev/null print_success "HTTP provisioning configured" echo "" echo "Access provisioning files at:" echo " HTTP: http://$(hostname -I | cut -d' ' -f1):8088/static/" echo " HTTPS: https://$(hostname -I | cut -d' ' -f1):8089/static/" } create_linphone_xml() { print_header "Create/Edit Linphone Provisioning XML" load_config local xml_file="$PROVISIONING_DIR/linphone.xml" local server_ip=$(hostname -I | cut -d' ' -f1) local domain="${DOMAIN_NAME:-$server_ip}" local transport="tcp" if [[ "$ENABLE_TLS" == "y" && -n "$DOMAIN_NAME" ]]; then transport="tls" fi echo "Current Configuration:" echo " Domain: $domain" echo " Transport: $transport" echo " Server IP: $server_ip" echo "" read -p "Create/Update linphone.xml? (y/n) [y]: " create_xml create_xml=${create_xml:-y} if [[ "$create_xml" =~ ^[Yy]$ ]]; then mkdir -p "$PROVISIONING_DIR" cat > "$xml_file" << EOF 0 1 0 <sip:${domain};transport=${transport}> sip:USERNAME@${domain} 3600 0 0 USERNAME PASSWORD ${domain} 7078 60 ANDROID SND: Android Sound card ANDROID SND: Android Sound card ANDROID SND: Android Sound card 0 0 0 1 0 1 0 0 1 1 0 1 1300 EOF chown asterisk:asterisk "$xml_file" chmod 644 "$xml_file" print_success "Created: $xml_file" echo "" echo "Provisioning URL:" if [[ "$transport" == "tls" ]]; then echo " https://${domain}:8089/static/linphone.xml" else echo " http://${server_ip}:8088/static/linphone.xml" fi echo "" echo "IMPORTANT for Android:" echo " 1. Use the URL above in Linphone's 'Remote provisioning'" echo " 2. Replace USERNAME and PASSWORD in device-specific XML files" echo " 3. Set Battery Optimization to 'Unrestricted' manually on phone" echo " 4. The XML prevents audio pause when screen turns off" echo "" echo "FOR /e/OS (eFoundation) users:" echo " See 'Troubleshoot /e/OS Audio' in Provisioning Manager menu" fi } edit_linphone_xml() { local xml_file="$PROVISIONING_DIR/linphone.xml" if [[ ! -f "$xml_file" ]]; then print_error "linphone.xml does not exist. Create it first." return 1 fi print_header "Edit Linphone XML" echo "Opening in nano editor..." echo "Press Ctrl+X to save and exit" echo "" read -p "Press Enter to continue..." nano "$xml_file" print_success "Changes saved" } show_provisioning_status() { print_header "Provisioning Status" # Check HTTP configuration if [[ -f /etc/asterisk/http.conf ]] && grep -q "enabled=yes" /etc/asterisk/http.conf 2>/dev/null; then echo -e "HTTP Server: ${GREEN}Enabled${NC}" asterisk -rx "http show status" 2>/dev/null | head -10 else echo -e "HTTP Server: ${RED}Disabled${NC}" fi echo "" # Check provisioning directory if [[ -d "$PROVISIONING_DIR" ]]; then echo -e "Provisioning Dir: ${GREEN}$PROVISIONING_DIR${NC}" echo "Files:" ls -lh "$PROVISIONING_DIR" 2>/dev/null | tail -n +2 || echo " (empty)" else echo -e "Provisioning Dir: ${RED}Not created${NC}" fi echo "" # Check symlink if [[ -L /usr/share/asterisk/static-http ]]; then echo -e "Symlink: ${GREEN}OK${NC} (/usr/share/asterisk/static-http)" else echo -e "Symlink: ${YELLOW}Not created${NC}" fi echo "" local server_ip=$(hostname -I | cut -d' ' -f1) echo "Provisioning URLs:" echo " HTTP: http://${server_ip}:8088/static/" echo " HTTPS: https://${server_ip}:8089/static/" } troubleshoot_eos_audio() { print_header "/e/OS Audio Troubleshooting" cat << 'EOSHELP' PROBLEM: No audio sent by phone unless Linphone has focus ═══════════════════════════════════════════════════════════ This is a known issue with /e/OS (eFoundation OS) and privacy-focused Android ROMs. /e/OS has stricter privacy controls that prevent apps from accessing the microphone in the background. SOLUTIONS (Try in order): 1. LINPHONE APP SETTINGS (In Linphone app itself): ──────────────────────────────────────────────────── a) Open Linphone → ☰ Menu → Settings → Audio b) Change "Audio Route" to "Speaker" (not Earpiece) c) Enable "Use Speaker for calls" d) Disable "Echo Cancellation" (test if this helps) e) Go to Settings → Network f) Set "Media Encryption" to "None" (or match server) 2. /e/OS PRIVACY SETTINGS: ──────────────────────────────────────────────────── a) Settings → Apps → Linphone b) Permissions → Microphone → "Allow all the time" c) Permissions → Camera → "Don't allow" (if not using video) d) "Remove permissions if app isn't used" → DISABLE 3. /e/OS ADVANCED PRIVACY SETTINGS: ──────────────────────────────────────────────────── a) Settings → Privacy (Advanced Privacy / Privacy Central) b) Find Linphone in the list c) Disable "Hide my IP" for Linphone d) Set Location to "Real" (not fake location) e) Disable any "Manage trackers" restrictions for Linphone 4. /e/OS NETWORK PERMISSIONS: ──────────────────────────────────────────────────── a) Settings → Apps → Linphone → Mobile data & Wi-Fi b) Enable "Background data" c) Enable "Unrestricted data usage" d) Make sure "Allow network access" is ON 5. /e/OS AUTOSTART: ──────────────────────────────────────────────────── a) Settings → Apps → Linphone → Battery b) Battery optimization → "Don't optimize" or "Unrestricted" c) Settings → Apps → Linphone → Advanced d) Enable "Autostart" if available 6. LINPHONE XML PROVISIONING (Server-side fix): ──────────────────────────────────────────────────── Your linphone.xml should already have these settings: • android_pause_calls_when_audio_focus_lost=0 • keep_service_alive=1 • start_at_boot=1 • audio_route_speaker=1 To verify, check: $PROVISIONING_DIR/linphone.xml 7. ALTERNATIVE: USE SPEAKER MODE DURING CALL: ──────────────────────────────────────────────────── As a workaround, during an active call: • Tap the speaker icon to enable speakerphone • This often forces audio to work even in background • Not ideal but proves the audio path works 8. NUCLEAR OPTION - DISABLE PRIVACY FEATURES: ──────────────────────────────────────────────────── If nothing works, temporarily disable /e/OS privacy features: a) Settings → Privacy → Advanced Privacy b) Toggle OFF "Advanced Privacy" c) Test if Linphone audio works d) If it works, re-enable and whitelist Linphone 9. ALTERNATIVE SIP APP: ──────────────────────────────────────────────────── If Linphone continues to have issues on /e/OS, try: • Zoiper (better /e/OS compatibility) • CSipSimple (older but reliable) • Grandstream Wave (commercial but works well) TESTING: ════════ 1. Make a call with Linphone in foreground → audio works 2. Press Home button → does audio continue? 3. If audio stops, the issue is confirmed WHAT'S HAPPENING: ═════════════════ /e/OS restricts background microphone access for privacy. Even with permissions granted, the OS may suspend audio capture when the app loses focus. The XML settings and speaker mode help work around this limitation. MORE HELP: ══════════ • /e/OS Community: https://community.e.foundation • Linphone Forums: https://forum.linphone.org • Issue: "Background microphone access on /e/OS" ═══════════════════════════════════════════════════════════ EOSHELP } create_baresip_config() { print_header "Create Baresip Setup Instructions" load_config local server_ip=$(hostname -I | cut -d' ' -f1) local domain="${DOMAIN_NAME:-$server_ip}" echo "Baresip Setup Guide Generator" echo "================================================" echo "" echo "Use Baresip if Linphone has audio issues (screen off, etc.)" echo "Baresip often works better on privacy-focused Android ROMs." echo "" echo "NOTE: Baresip does NOT support remote provisioning." echo " Manual configuration required." echo "" echo "Current Configuration:" echo " Domain: $domain" echo " Server IP: $server_ip" echo "" read -p "Enter extension number (e.g., 202): " extension [[ -z "$extension" ]] && { print_error "Extension required"; return 1; } read -p "Enter SIP password: " sip_password [[ -z "$sip_password" ]] && { print_error "Password required"; return 1; } read -p "Enter display name (e.g., Kitchen Phone): " display_name display_name=${display_name:-Extension $extension} local config_file="$PROVISIONING_DIR/baresip-${extension}.txt" mkdir -p "$PROVISIONING_DIR" cat > "$config_file" << BARESIPEOF ═══════════════════════════════════════════════════════════ BARESIP SETUP INSTRUCTIONS Generated by Easy Asterisk v${SCRIPT_VERSION} ═══════════════════════════════════════════════════════════ IMPORTANT: Baresip does NOT support remote provisioning. You must configure manually following these steps. STEP 1: INSTALL BARESIP ════════════════════════════════════════════════════════════ • Download Baresip from F-Droid or Play Store • Open the Baresip app STEP 2: ADD ACCOUNT (Initial Entry) ════════════════════════════════════════════════════════════ 1. Tap Menu (☰ hamburger icon) → Accounts 2. Tap the Add (+) button at the top 3. In "SIP URI" field, enter: BARESIPEOF echo "${extension}@${domain}" >> "$config_file" cat >> "$config_file" << 'BARESIPEOF' 4. Tap the Save (✓ checkmark) icon at the top STEP 3: EDIT ACCOUNT (Complete Configuration) ════════════════════════════════════════════════════════════ Now go back and edit the account to add authentication: 1. Tap Menu (☰) → Accounts 2. Tap on the account you just created 3. Fill in the following fields: BARESIPEOF cat >> "$config_file" << EOF Display Name: ${display_name} Authentication Username: ${extension} (CRITICAL: Just the extension number, NOT ${extension}@${domain}) Authentication Password: ${sip_password} Outbound Proxy URI: ${domain} (CRITICAL: Just the domain, NOT sip:${server_ip}:5060) Media Encryption: srtp (Select from dropdown menu) Register: ✓ (Check this box) 4. Tap Save (✓ checkmark icon) STEP 4: VERIFY REGISTRATION ════════════════════════════════════════════════════════════ • Wait a few seconds for registration • You should see: - Green dot next to account, OR - "Registered" status text If registration FAILS: ✗ Double-check "Authentication Username" is JUST "${extension}" ✗ Double-check "Outbound Proxy URI" is JUST "${domain}" ✗ Verify password is correct: ${sip_password} STEP 5: SET CALLING AS DEFAULT (Optional) ════════════════════════════════════════════════════════════ To make tapping a contact initiate a call (not message): 1. Tap Menu (☰) → Settings (or Preferences) 2. Look for "Default Action" or "Contact Action" 3. If available, select: "Audio Call" or "Call" 4. Save NOTE: This option may not exist in all Baresip versions. If not available, you can still call by: - Long-pressing a contact → Select "Call" - Or using the phone icon during selection STEP 6: AUDIO SETTINGS (Recommended) ════════════════════════════════════════════════════════════ 1. Tap Menu (☰) → Settings → Audio 2. Configure: Audio Module: opensles (or audiotrack if opensles doesn't work) Echo Cancellation: ✓ Enabled Noise Suppression: ✓ Enabled STEP 7: ANDROID PERMISSIONS ════════════════════════════════════════════════════════════ Go to your phone's: Settings → Apps → Baresip Set the following: • Permissions → Microphone: Allow while using app • Permissions → Phone: Allow • Battery: Unrestricted (or Not optimized) • Mobile data & Wi-Fi → Background data: Enabled DIALING EXTENSIONS ════════════════════════════════════════════════════════════ To call other extensions: Method 1 (Try this first): Just dial the extension number: 101, 202, etc. Method 2 (If method 1 doesn't work): Full format: 101@${domain} Common Extensions: • Individual devices: 101, 102, 201, 202, etc. • Page groups (auto-answer broadcast): 199 • Ring groups (rings all phones): 299 TOP BAR ICONS IN BARESIP ════════════════════════════════════════════════════════════ ☰ = Hamburger menu (Accounts, Settings, About, etc.) ✓ = Save/Confirm current action ⋮ = Additional options (context-dependent) 📞 = Answer incoming call / Place outgoing call 🔊 = Enable speakerphone (during active call) 🔇 = Mute microphone (during active call) ✕ = Hang up / End call TROUBLESHOOTING ════════════════════════════════════════════════════════════ PROBLEM: Registration fails SOLUTION: • Verify "Authentication Username" is JUST: ${extension} • Verify "Outbound Proxy URI" is JUST: ${domain} • Check password is correct • Check phone has network connectivity • Check firewall allows SIP traffic PROBLEM: Can't dial extensions SOLUTION: • Verify you're registered (green dot/status) • Try dialing full format: ${extension}@${domain} • Check extension exists on server PROBLEM: No audio / Audio doesn't work SOLUTION: • Menu → Settings → Audio → Try different "Audio Module" • Check microphone permissions in Android settings • During call, try tapping speaker icon PROBLEM: Audio cuts when screen turns off SOLUTION: • Settings → Apps → Baresip → Battery → Unrestricted • Baresip handles this much better than Linphone! • This issue is rare with Baresip PROBLEM: Can't find "Default Action" setting SOLUTION: • Not all Baresip versions have this option • Alternative: Long-press contact → Select "Call" • Or tap contact then tap phone icon ═══════════════════════════════════════════════════════════ QUICK REFERENCE ═══════════════════════════════════════════════════════════ Display Name: ${display_name} SIP URI (initial): ${extension}@${domain} Auth Username: ${extension} Auth Password: ${sip_password} Outbound Proxy: ${domain} Media Encryption: srtp Register: ✓ ═══════════════════════════════════════════════════════════ EOF chown asterisk:asterisk "$config_file" chmod 644 "$config_file" print_success "Created: $config_file" echo "" echo "═══════════════════════════════════════════════════════════" echo "BARESIP SETUP - Extension ${extension}" echo "═══════════════════════════════════════════════════════════" echo "" echo "Download instructions:" echo " http://${server_ip}:8088/static/baresip-${extension}.txt" echo "" echo "QUICK SETUP SUMMARY:" echo "" echo "Step 1: Add Account" echo " Menu → Accounts → Add (+)" echo " SIP URI: ${extension}@${domain}" echo " Save (✓)" echo "" echo "Step 2: Edit Account" echo " Tap account → Edit" echo " Auth Username: ${extension} (JUST the number!)" echo " Auth Password: ${sip_password}" echo " Outbound Proxy: ${domain} (JUST the domain!)" echo " Media Encryption: srtp (select from dropdown)" echo " Register: ✓" echo " Save (✓)" echo "" echo "Step 3: Verify" echo " Look for green dot or 'Registered' status" echo "" echo "Step 4: Dial Extensions" echo " Just dial: 101, 202, etc." echo "" echo "Full details in the text file above." echo "═══════════════════════════════════════════════════════════" } provisioning_manager_menu() { while true; do clear print_header "Provisioning Manager" echo " 1) Setup HTTP Server (ports 8088/8089)" echo " 2) Create/Update linphone.xml" echo " 3) Edit linphone.xml" echo " 4) Create Baresip Config" echo " 5) Show Status" echo " 6) Open Provisioning Directory" echo " 7) Troubleshoot /e/OS Audio Issues" echo " 0) Back" read -p " Select: " choice case $choice in 1) setup_http_provisioning ;; 2) create_linphone_xml ;; 3) edit_linphone_xml ;; 4) create_baresip_config ;; 5) show_provisioning_status ;; 6) if command -v mc &>/dev/null; then mc "$PROVISIONING_DIR" else print_info "Opening with ls..." ls -lah "$PROVISIONING_DIR" fi ;; 7) troubleshoot_eos_audio ;; 0) return ;; esac [[ "$choice" != "0" ]] && read -p "Press Enter..." done } # ================================================================ # MANUAL UPDATE SYSTEM # ================================================================ manual_update_asterisk() { print_header "Manual Asterisk Update" echo "WARNING: This will update Asterisk from the repository." echo "A backup will be created automatically." echo "" asterisk -V 2>/dev/null || echo "Asterisk not currently running" echo "" read -p "Continue with update? (y/n) [n]: " confirm confirm=${confirm:-n} if [[ ! "$confirm" =~ ^[Yy]$ ]]; then print_info "Update cancelled" return fi # Backup configurations local backup_dir="/root/asterisk-backup-$(date +%Y%m%d_%H%M%S)" mkdir -p "$backup_dir" echo "Creating backup in $backup_dir..." cp -r /etc/asterisk "$backup_dir/" cp -r /var/lib/asterisk "$backup_dir/" 2>/dev/null || true print_success "Backup created: $backup_dir" # Update echo "" print_info "Updating Asterisk..." apt update apt install --only-upgrade asterisk asterisk-modules -y # Restart echo "" print_info "Restarting Asterisk..." systemctl restart asterisk sleep 3 if systemctl is-active asterisk >/dev/null; then print_success "Asterisk updated successfully" asterisk -V echo "" echo "Backup location: $backup_dir" echo "" echo "To rollback if needed:" echo " systemctl stop asterisk" echo " cp -r $backup_dir/asterisk/* /etc/asterisk/" echo " systemctl start asterisk" else print_error "Asterisk failed to start after update!" echo "" echo "Rolling back..." cp -r "$backup_dir/asterisk/"* /etc/asterisk/ systemctl restart asterisk print_info "Rollback complete" fi } # ================================================================ # ROOM DIRECTORY # ================================================================ show_room_directory() { print_header "Room Directory" load_config if [[ ! -f "$ROOMS_FILE" ]]; then print_error "Rooms file not found: $ROOMS_FILE" return fi echo "Ring Groups vs Page Groups:" echo " • Ring Groups: Rings all members until one answers" echo " • Page Groups: Auto-answer broadcast to all members" echo "" echo "═══════════════════════════════════════════════════════════" local has_rooms=false while IFS='|' read -r ext name members timeout type; do # Skip comments and empty lines [[ "$ext" =~ ^[[:space:]]*# ]] && continue [[ -z "$ext" ]] && continue has_rooms=true # Determine icon based on type local icon="📞" local type_label="Ring Group" if [[ "$type" == "page" ]]; then icon="📢" type_label="Page Group" fi echo "" echo "$icon Extension: $ext - $name" echo " Type: $type_label" echo " Members: $members" echo " Timeout: ${timeout}s" done < "$ROOMS_FILE" if [[ "$has_rooms" == "false" ]]; then echo "" echo "No rooms configured yet." echo "Use 'Device Management → Manage rooms' to create rooms." fi echo "" echo "═══════════════════════════════════════════════════════════" } watch_live_logs() { print_header "Live Debugging" echo "Enabling PJSIP Logger..." asterisk -rx "module load res_pjsip_logger.so" 2>/dev/null || true asterisk -rx "pjsip set logger on" 2>/dev/null echo "" echo "Options:" echo " 1) Asterisk Console (verbose)" echo " 2) Packet Capture (tcpdump)" read -p "Select [1]: " pcap if [[ "$pcap" == "2" ]]; then echo "Starting tcpdump. Press CTRL+C to stop." tcpdump -i any port 5060 or port 5061 -nn -v else echo "Starting Console. Press CTRL+C to exit." asterisk -rvvv fi asterisk -rx "pjsip set logger off" 2>/dev/null } router_doctor() { print_header "Router Traffic Doctor" if ! systemctl is-active asterisk >/dev/null; then print_error "Asterisk is NOT RUNNING" restart_asterisk_safe return fi print_success "Asterisk is UP" echo "" echo "Server Listening IPs:" ip -o -4 addr show | awk '{print " " $2 ": " $4}' echo "" echo "Instructions:" echo " 1. Take out your phone/laptop" echo " 2. Attempt to REGISTER or CALL" echo " 3. I will listen for 15 seconds" echo "" read -p "Press Enter to start listening..." if timeout 15 tcpdump -i any -c 1 "port 5060 or port 5061" 2>/dev/null; then echo "" print_success "PACKET RECEIVED! Router forwarding is working." else echo "" print_error "NO PACKETS RECEIVED." echo "Your router or firewall is blocking the connection." fi } configure_local_client() { print_header "Configure Local Client" load_config # If KIOSK_USER already set from config, show and ask if want to change if [[ -n "$KIOSK_USER" ]]; then echo "Current configured user: $KIOSK_USER" read -p "Change user? [y/N]: " change_user if [[ "$change_user" =~ ^[Yy]$ ]]; then KIOSK_USER="" KIOSK_UID="" fi fi # If still no user, select one if [[ -z "$KIOSK_USER" ]]; then echo "" echo "Select the user to configure:" echo "" if ! select_user; then print_error "User selection failed" return 1 fi else # Ensure KIOSK_UID is set KIOSK_UID=$(id -u "$KIOSK_USER" 2>/dev/null) fi echo "" if [[ ! -d "/home/${KIOSK_USER}/.baresip" ]]; then print_error "Baresip not installed for $KIOSK_USER" echo "" read -p "Install Baresip client now? [Y/n]: " install_it if [[ ! "$install_it" =~ ^[Nn]$ ]]; then install_baresip_packages configure_baresip enable_client_services INSTALLED_CLIENT="y" save_config print_success "Baresip installed" echo "" echo "Audio configured for $KIOSK_USER" echo "If audio doesn't work, log out and back in or reboot." echo "" else return fi fi read -p "Extension: " ext read -p "Password: " pass read -p "Server Domain/IP: " server local transport_str="udp" local media_enc="" if [[ "$server" =~ [a-zA-Z] ]]; then print_info "Domain detected. Using TLS." transport_str="tls" media_enc=";mediaenc=srtp" fi echo "" echo "Answer Mode:" echo " 1) Manual (ring on incoming)" echo " 2) Auto (auto-answer)" read -p "Select [1]: " amode local answermode="manual" [[ "$amode" == "2" ]] && answermode="auto" echo "" echo "Enable TURN? (Required if behind NAT/VLAN without VPN)" read -p "Use TURN server? [y/N]: " use_turn local turn_config="" if [[ "$use_turn" =~ ^[Yy]$ ]]; then read -p "TURN User [${TURN_USER}]: " t_user t_user="${t_user:-$TURN_USER}" read -p "TURN Pass [${TURN_PASS}]: " t_pass t_pass="${t_pass:-$TURN_PASS}" local turn_host="${server}" if [[ ! "$turn_host" =~ [a-zA-Z] ]]; then # If server is IP, ask if TURN host is different read -p "TURN Host [${server}]: " th turn_host="${th:-$server}" fi turn_config="turn_server turn:${t_user}:${t_pass}@${turn_host}:3478" fi # Update config file for TURN local conf_file="/home/${KIOSK_USER}/.baresip/config" if [[ -f "$conf_file" ]]; then sed -i '/^turn_server/d' "$conf_file" if [[ -n "$turn_config" ]]; then echo "$turn_config" >> "$conf_file" print_success "TURN configuration added" fi fi cat > "/home/${KIOSK_USER}/.baresip/accounts" << EOF ;auth_pass=${pass};answermode=${answermode}${media_enc} EOF chown ${KIOSK_USER}:${KIOSK_USER} "/home/${KIOSK_USER}/.baresip/accounts" chown ${KIOSK_USER}:${KIOSK_USER} "/home/${KIOSK_USER}/.baresip/config" # Update main config ASTERISK_HOST="$server" KIOSK_EXTENSION="$ext" CLIENT_ANSWERMODE="$answermode" save_config local user_dbus="XDG_RUNTIME_DIR=/run/user/${KIOSK_UID}" # Reload systemd daemon in case services changed sudo -u "${KIOSK_USER}" $user_dbus systemctl --user daemon-reload 2>/dev/null # Restart audio and client services print_info "Restarting services..." sudo -u "${KIOSK_USER}" $user_dbus systemctl --user restart pipewire pipewire-pulse 2>/dev/null || true sleep 2 sudo -u "${KIOSK_USER}" $user_dbus systemctl --user restart baresip 2>/dev/null # Ensure audio is unmuted if not in PTT mode if [[ ! -f /etc/easy-asterisk/ptt-device ]]; then sleep 1 ensure_audio_unmuted fi print_success "Client Reconfigured & Services Restarted" echo "" echo "Run Diagnostics to verify connection status." } run_client_diagnostics() { print_header "Client Diagnostics" load_config local t_user="${KIOSK_USER:-$SUDO_USER}" t_user="${t_user:-$USER}" local t_uid=$(id -u "$t_user" 2>/dev/null) echo -e "User: ${BOLD}$t_user${NC}" echo "---------------------------------------------------" if sudo -u "$t_user" XDG_RUNTIME_DIR=/run/user/$t_uid systemctl --user is-active baresip >/dev/null 2>&1; then print_success "Baresip RUNNING" else print_error "Baresip STOPPED/FAILED" fi echo "---------------------------------------------------" echo "Audio Services:" local user_dbus="XDG_RUNTIME_DIR=/run/user/$t_uid" if sudo -u "$t_user" $user_dbus systemctl --user is-active pipewire >/dev/null 2>&1; then print_success "PipeWire RUNNING" else print_error "PipeWire STOPPED" fi if sudo -u "$t_user" $user_dbus systemctl --user is-active pipewire-pulse >/dev/null 2>&1; then print_success "PipeWire-Pulse RUNNING" else print_error "PipeWire-Pulse STOPPED" fi echo "" echo "Audio Status:" local src_mute=$(sudo -u "$t_user" $user_dbus pactl get-source-mute @DEFAULT_SOURCE@ 2>/dev/null | awk '{print $2}') local sink_mute=$(sudo -u "$t_user" $user_dbus pactl get-sink-mute @DEFAULT_SINK@ 2>/dev/null | awk '{print $2}') local src_vol=$(sudo -u "$t_user" $user_dbus pactl get-source-volume @DEFAULT_SOURCE@ 2>/dev/null | grep -oP '\d+%' | head -1) local sink_vol=$(sudo -u "$t_user" $user_dbus pactl get-sink-volume @DEFAULT_SINK@ 2>/dev/null | grep -oP '\d+%' | head -1) echo " Microphone: ${src_mute:-unknown} (Volume: ${src_vol:-unknown})" echo " Speaker: ${sink_mute:-unknown} (Volume: ${sink_vol:-unknown})" if [[ "$src_mute" == "yes" ]]; then echo "" print_error "MICROPHONE IS MUTED - No audio will be sent!" echo " To fix: pactl set-source-mute @DEFAULT_SOURCE@ 0" fi echo "" echo "PTT Configuration:" if [[ -f /etc/easy-asterisk/ptt-device ]]; then echo " PTT Mode: ENABLED" source /etc/easy-asterisk/ptt-device 2>/dev/null echo " Device: ${PTT_DEVICE:-not set}" else echo " PTT Mode: DISABLED (normal intercom mode)" fi echo "---------------------------------------------------" echo "Network Interface:" grep "^net_interface" "/home/$t_user/.baresip/config" 2>/dev/null || echo " Not set" echo "---------------------------------------------------" echo "Account Config:" cat "/home/$t_user/.baresip/accounts" 2>/dev/null | sed 's/auth_pass=[^;]*/auth_pass=***/' || echo " Not found" echo "---------------------------------------------------" if [[ -n "$ASTERISK_HOST" ]]; then echo -n "Server ($ASTERISK_HOST): " if ping -c 1 -W 2 "$ASTERISK_HOST" >/dev/null 2>&1; then print_success "Reachable" else print_error "Unreachable" fi fi echo "---------------------------------------------------" echo "System Logs (launcher):" journalctl -t baresip-launcher -n 10 --no-pager 2>/dev/null | tail -10 || echo " No launcher logs" echo "" echo "System Logs (PTT):" journalctl -t kiosk-ptt -n 5 --no-pager 2>/dev/null | tail -5 || echo " No PTT logs" echo "" echo "Baresip Service Log:" sudo -u "$t_user" journalctl --user -u baresip -n 10 --no-pager 2>/dev/null || echo " No logs" echo "---------------------------------------------------" echo "" echo "To see live logs, run:" echo " journalctl -t baresip-launcher -f # Launcher logs" echo " journalctl -t kiosk-ptt -f # PTT logs" echo " sudo -u $t_user journalctl --user -u baresip -f # Baresip logs" echo "---------------------------------------------------" } run_audio_test() { print_header "Audio Test" echo "Playing test tone..." speaker-test -t sine -f 440 -c 2 -l 1 >/dev/null 2>&1 echo "" read -p "Did you hear audio? [y/N]: " res if [[ "$res" =~ ^[Yy]$ ]]; then print_success "Audio OK" else print_error "Check volume/connections" fi } verify_audio_setup() { print_header "Audio Verification" echo "=== Codecs ===" asterisk -rx "core show codecs" 2>/dev/null | grep -E "(opus|ulaw|alaw|g722)" || echo " N/A" echo "" echo "=== PJSIP Modules ===" asterisk -rx "module show like pjsip" 2>/dev/null | head -10 || echo " N/A" echo "" echo "=== Certificate ===" if [[ -f /etc/asterisk/certs/server.crt ]]; then openssl x509 -in /etc/asterisk/certs/server.crt -noout -subject -dates 2>/dev/null else echo " None" fi } # ================================================================ # 7. ASTERISK CONFIG # ================================================================ fix_asterisk_systemd() { print_info "Configuring systemd..." mkdir -p /etc/systemd/system/asterisk.service.d/ cat > /etc/systemd/system/asterisk.service.d/override.conf << 'SVCEOF' [Unit] Wants=network-online.target After=network-online.target [Service] ExecStart= ExecStart=/usr/sbin/asterisk -f -U asterisk -G asterisk RuntimeDirectory=asterisk RuntimeDirectoryMode=0750 MemoryMax=infinity TasksMax=infinity KillMode=mixed KillSignal=SIGTERM TimeoutStartSec=60 TimeoutStopSec=30 SendSIGKILL=no Restart=always RestartSec=10 Type=simple SVCEOF systemctl daemon-reload } recover_xml_docs() { mkdir -p /var/lib/asterisk/documentation/thirdparty chown -R asterisk:asterisk /var/lib/asterisk/documentation 2>/dev/null } repair_core_configs() { print_info "Repairing configs..." # Copy modules (not symlink - AppArmor blocks symlinks) if [[ -d "/usr/lib/x86_64-linux-gnu/asterisk/modules" ]]; then mkdir -p /usr/lib/asterisk/modules cp -rn /usr/lib/x86_64-linux-gnu/asterisk/modules/* /usr/lib/asterisk/modules/ 2>/dev/null || true fi mkdir -p /etc/asterisk /var/lib/asterisk /var/log/asterisk /var/spool/asterisk /var/run/asterisk recover_xml_docs if [[ ! -f /etc/asterisk/asterisk.conf ]]; then cat > /etc/asterisk/asterisk.conf << EOF [directories] astetcdir => /etc/asterisk astmoddir => /usr/lib/asterisk/modules astvarlibdir => /var/lib/asterisk astdbdir => /var/lib/asterisk astkeydir => /var/lib/asterisk astdatadir => /var/lib/asterisk astagidir => /var/lib/asterisk/agi-bin astspooldir => /var/spool/asterisk astrundir => /var/run/asterisk astlogdir => /var/log/asterisk EOF fi cat > /etc/asterisk/modules.conf << EOF [modules] autoload=yes noload => chan_sip.so noload => chan_iax2.so load => res_pjsip.so load => res_pjsip_session.so load => res_pjsip_logger.so load => chan_pjsip.so load => codec_ulaw.so load => codec_alaw.so load => codec_g722.so load => codec_opus.so load => res_rtp_asterisk.so load => app_dial.so load => app_page.so load => pbx_config.so EOF # Disable optional modules (NOT stasis - required in Asterisk 20.x) for conf in ari http manager geolocation; do cat > "/etc/asterisk/${conf}.conf" << EOF [general] enabled = no EOF done # Configure Stasis properly (required core module) cat > /etc/asterisk/stasis.conf << EOF [general] ; Stasis is required for Asterisk 20.x core functionality EOF if [[ ! -f /etc/asterisk/sorcery.conf ]]; then cat > /etc/asterisk/sorcery.conf << EOF [res_pjsip] endpoint=config,pjsip.conf,criteria=type=endpoint auth=config,pjsip.conf,criteria=type=auth aor=config,pjsip.conf,criteria=type=aor transport=config,pjsip.conf,criteria=type=transport EOF fi # ICE and STUN only for FQDN/internet calling load_config local ice_stun_config="" if [[ -n "$DOMAIN_NAME" ]]; then ice_stun_config="icesupport=yes stunaddr=stun.l.google.com:19302" else ice_stun_config="# icesupport disabled - LAN only mode" fi cat > /etc/asterisk/rtp.conf << EOF [general] rtpstart=10000 rtpend=20000 strictrtp=yes ${ice_stun_config} EOF cat > /etc/asterisk/logger.conf << EOF [general] [logfiles] console => notice,warning,error EOF rm -f /var/lib/asterisk/.asterisk_history chown -R asterisk:asterisk /etc/asterisk /var/lib/asterisk /var/log/asterisk /var/spool/asterisk 2>/dev/null || true chown -R asterisk:asterisk /usr/lib/asterisk/modules 2>/dev/null || true } generate_pjsip_conf() { print_info "Generating PJSIP..." load_config local conf_file="/etc/asterisk/pjsip.conf" backup_config "$conf_file" # Prioritize CURRENT_PUBLIC_IP from coturn/updater if available, else detect local public_ip="${CURRENT_PUBLIC_IP}" if [[ -z "$public_ip" ]]; then public_ip=$(curl -s -4 --connect-timeout 5 ifconfig.me 2>/dev/null || echo "") fi # Get server IP for transport binding info local server_ip=$(hostname -I | cut -d' ' -f1) local raw_cidr=$(ip -o -f inet addr show | awk '/scope global/ {print $4}' | head -1) local default_cidr="$raw_cidr" if [[ "$raw_cidr" =~ \.([0-9]+)/24$ ]]; then default_cidr="${raw_cidr%.*}.0/24"; fi # Use stored CIDR if available local local_net="${LOCAL_CIDR:-$default_cidr}" # Build local_net entries (main network + VLANs) local all_local_nets="local_net=$local_net" if [[ "$HAS_VLANS" == "y" && -n "$VLAN_SUBNETS" ]]; then for vlan_subnet in $VLAN_SUBNETS; do all_local_nets="${all_local_nets} local_net=${vlan_subnet}" done print_info "VLAN subnets configured: $VLAN_SUBNETS" fi local nat_settings="" if [[ -n "$public_ip" && -n "$DOMAIN_NAME" ]]; then nat_settings="external_media_address=$public_ip external_signaling_address=$public_ip ${all_local_nets}" print_info "NAT: Public IP=$public_ip, Server IP=$server_ip" fi cat > "$conf_file" << EOF ; Easy Asterisk v${SCRIPT_VERSION} [global] type=global user_agent=EasyAsterisk [transport-udp] type=transport protocol=udp bind=0.0.0.0:${DEFAULT_SIP_PORT} ; Server IP: ${server_ip} ${nat_settings} [transport-tcp] type=transport protocol=tcp bind=0.0.0.0:${DEFAULT_SIP_PORT} ; Server IP: ${server_ip} ${nat_settings} [transport-tls] type=transport protocol=tls bind=0.0.0.0:${DEFAULT_SIPS_PORT} ; Server IP: ${server_ip} cert_file=/etc/asterisk/certs/server.crt priv_key_file=/etc/asterisk/certs/server.key ca_list_file=/etc/ssl/certs/ca-certificates.crt method=tlsv1_2 ${nat_settings} EOF local backup_file=$(ls -t "${conf_file}.backup-"* 2>/dev/null | head -1) if [[ -f "$backup_file" ]]; then awk '/^; === Device:/{flag=1} flag' "$backup_file" >> "$conf_file" print_success "Restored devices from backup" fi chown asterisk:asterisk "$conf_file" } rebuild_dialplan() { local quiet=$1 [[ "$quiet" != "quiet" ]] && print_info "Rebuilding dialplan..." local conf_file="/etc/asterisk/extensions.conf" backup_config "$conf_file" cat > "$conf_file" << EOF [general] static=yes writeprotect=no [default] exten => _X.,1,Hangup() [intercom] EOF local dev_name="" dev_cat="" dev_auto="" dev_aa_override="" while IFS= read -r line; do if [[ "$line" == *"; === Device:"* ]]; then dev_aa_override="" local temp="${line#*; === Device: }" temp="${temp% ===}" if [[ "$temp" == *"[AA:yes]"* ]]; then dev_aa_override="yes"; temp="${temp% [AA:yes]}" elif [[ "$temp" == *"[AA:no]"* ]]; then dev_aa_override="no"; temp="${temp% [AA:no]}" fi dev_cat="${temp##* (}"; dev_cat="${dev_cat%)}" dev_name="${temp% (*)}" dev_auto="no" local cat_data=$(grep "^${dev_cat}|" "$CATEGORIES_FILE" 2>/dev/null || true) if [[ -n "$cat_data" ]]; then local is_auto=$(echo "$cat_data" | cut -d'|' -f3) [[ "$is_auto" == "yes" ]] && dev_auto="yes" fi [[ "$dev_aa_override" == "yes" ]] && dev_auto="yes" [[ "$dev_aa_override" == "no" ]] && dev_auto="no" fi if [[ "$line" =~ ^\[([0-9]+)\] ]]; then local ext="${BASH_REMATCH[1]}" if [[ -n "$dev_name" ]]; then if [[ "$dev_auto" == "yes" ]]; then cat >> "$conf_file" << EOF exten => ${ext},1,NoOp(Auto-Answer ${ext}) same => n,Set(PJSIP_HEADER(add,Call-Info)=\;answer-after=0) same => n,Set(PJSIP_HEADER(add,Alert-Info)=auto-answer) same => n,Dial(PJSIP/${ext},60) same => n,Hangup() EOF else cat >> "$conf_file" << EOF exten => ${ext},1,NoOp(Call ${ext}) same => n,Dial(PJSIP/${ext},60) same => n,Hangup() EOF fi dev_name="" fi fi done < /etc/asterisk/pjsip.conf # Add rooms if [[ -f "$ROOMS_FILE" ]]; then while IFS='|' read -r rext rname rmem rtime rtype; do [[ "$rext" =~ ^# ]] && continue [[ -z "$rext" ]] && continue local dial_list="" IFS=',' read -ra EXTS <<< "$rmem" for ext in "${EXTS[@]}"; do ext=$(echo "$ext" | tr -d ' ') [[ -n "$dial_list" ]] && dial_list="${dial_list}&" dial_list="${dial_list}PJSIP/${ext}" done if [[ "$rtype" == "page" ]]; then cat >> "$conf_file" << EOF ; Room: ${rname} (Page) exten => ${rext},1,NoOp(Page ${rname}) same => n,Set(PJSIP_HEADER(add,Call-Info)=\;answer-after=0) same => n,Page(${dial_list},i,${rtime}) same => n,Hangup() EOF else cat >> "$conf_file" << EOF ; Room: ${rname} (Ring) exten => ${rext},1,NoOp(Call ${rname}) same => n,Dial(${dial_list},${rtime}) same => n,Hangup() EOF fi done < "$ROOMS_FILE" fi chown -R asterisk:asterisk /etc/asterisk asterisk -rx "dialplan reload" &>/dev/null || true } configure_asterisk() { if ! id asterisk >/dev/null 2>&1; then useradd -r -s /bin/false -d /var/lib/asterisk asterisk 2>/dev/null || true fi print_info "Configuring Asterisk..." fix_asterisk_systemd initialize_default_categories repair_core_configs mkdir -p /etc/asterisk/certs if [[ ! -f /etc/asterisk/certs/server.crt ]]; then openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ -keyout /etc/asterisk/certs/server.key \ -out /etc/asterisk/certs/server.crt \ -subj "/CN=asterisk-local" 2>/dev/null fi chown asterisk:asterisk /etc/asterisk/certs/server.* 2>/dev/null || true chmod 644 /etc/asterisk/certs/server.crt 2>/dev/null || true chmod 600 /etc/asterisk/certs/server.key 2>/dev/null || true generate_pjsip_conf rebuild_dialplan "quiet" restart_asterisk_safe systemctl enable asterisk } restart_asterisk_safe() { print_info "Restarting Asterisk..." systemctl stop asterisk 2>/dev/null || true sleep 2 # Use -x for exact match to avoid killing this script pkill -9 -x asterisk 2>/dev/null || true rm -f /var/run/asterisk/asterisk.pid 2>/dev/null || true rm -f /var/lib/asterisk/.asterisk_history 2>/dev/null || true systemctl start asterisk sleep 3 if systemctl is-active asterisk >/dev/null; then print_success "Asterisk running" else print_error "Asterisk failed to start" journalctl -u asterisk -n 15 --no-pager fi } # ================================================================ # 8. CLIENT CONFIG # ================================================================ configure_baresip() { local baresip_dir="/home/${KIOSK_USER}/.baresip" mkdir -p "$baresip_dir" # Detect network interface local found_iface="" for target in 8.8.8.8 1.1.1.1 9.9.9.9; do local iface=$(ip route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -1) if [[ -n "$iface" ]]; then found_iface="$iface" print_success "Network interface: $found_iface" break fi done cat > "${baresip_dir}/config" << EOF poll_method epoll audio_player pulse audio_source pulse audio_alert pulse sip_autoanswer yes sip_cafile /etc/ssl/certs/ca-certificates.crt rtp_timeout 0 net_af ipv4 module_path /usr/lib/baresip/modules module srtp.so module stdio.so module pulse.so module g711.so module opus.so module account.so module stun.so module ice.so module turn.so EOF [[ -n "$found_iface" ]] && echo "net_interface $found_iface" >> "${baresip_dir}/config" local transport="udp" local mediaenc="" if [[ "$ENABLE_TLS" == "y" ]]; then transport="tls" mediaenc=";mediaenc=srtp" fi local amode="${CLIENT_ANSWERMODE:-auto}" cat > "${baresip_dir}/accounts" << EOF ;auth_pass=${SIP_PASSWORD};answermode=${amode}${mediaenc} EOF chown -R ${KIOSK_USER}:${KIOSK_USER} "$baresip_dir" chmod 700 "$baresip_dir" configure_audio_ducking create_ptt_handler create_baresip_launcher } create_baresip_launcher() { local launcher_user="${KIOSK_USER}" cat > /usr/local/bin/easy-asterisk-launcher << LAUNCHER #!/bin/bash CONFIG_FILE="/home/${launcher_user}/.baresip/config" ACCOUNTS_FILE="/home/${launcher_user}/.baresip/accounts" TARGETS=("8.8.8.8" "1.1.1.1" "9.9.9.9") FOUND_IFACE="" logger -t baresip-launcher "Starting Baresip launcher for user ${launcher_user}" # Wait for network for i in {1..6}; do for target in "\${TARGETS[@]}"; do IFACE=\$(ip route get "\$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if(\$i=="dev") print \$(i+1)}' | head -1) if [[ -n "\$IFACE" ]]; then FOUND_IFACE="\$IFACE" logger -t baresip-launcher "Network found on interface: \$IFACE" break 2 fi done logger -t baresip-launcher "Waiting for network... (attempt \$i/6)" sleep 5 done if [[ -z "\$FOUND_IFACE" ]]; then logger -t baresip-launcher "ERROR: No network interface found after 30 seconds" fi # Update network interface in config if [[ -f "\$CONFIG_FILE" && -n "\$FOUND_IFACE" ]]; then sed -i '/^#*net_interface/d' "\$CONFIG_FILE" echo "net_interface \${FOUND_IFACE}" >> "\$CONFIG_FILE" logger -t baresip-launcher "Updated config with interface: \$FOUND_IFACE" fi # Verify config files exist if [[ ! -f "\$CONFIG_FILE" ]]; then logger -t baresip-launcher "ERROR: Config file not found: \$CONFIG_FILE" exit 1 fi if [[ ! -f "\$ACCOUNTS_FILE" ]]; then logger -t baresip-launcher "ERROR: Accounts file not found: \$ACCOUNTS_FILE" exit 1 fi logger -t baresip-launcher "Starting Baresip client..." exec /usr/bin/baresip -f "/home/${launcher_user}/.baresip" LAUNCHER chmod +x /usr/local/bin/easy-asterisk-launcher } enable_client_services() { local systemd_dir="/home/${KIOSK_USER}/.config/systemd/user" mkdir -p "$systemd_dir" # Ensure audio group membership if ! id -nG "$KIOSK_USER" | grep -qw "audio"; then usermod -aG audio "$KIOSK_USER" fi # Ensure input group membership (for PTT device access) if ! id -nG "$KIOSK_USER" | grep -qw "input"; then usermod -aG input "$KIOSK_USER" fi # Baresip service cat > "${systemd_dir}/baresip.service" << EOF [Unit] Description=Baresip SIP Client After=pipewire.service pipewire-pulse.service network-online.target Wants=network-online.target pipewire.service pipewire-pulse.service Requires=pipewire-pulse.service [Service] Type=simple ExecStartPre=/bin/sleep 5 ExecStart=/usr/local/bin/easy-asterisk-launcher Restart=always RestartSec=10 Environment=XDG_RUNTIME_DIR=/run/user/${KIOSK_UID} Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${KIOSK_UID}/bus StandardOutput=journal StandardError=journal [Install] WantedBy=default.target EOF # PTT service - only create if PTT is configured cat > "${systemd_dir}/kiosk-ptt.service" << EOF [Unit] Description=PTT Button Handler After=pipewire.service pipewire-pulse.service baresip.service Requires=pipewire-pulse.service ConditionPathExists=/etc/easy-asterisk/ptt-device [Service] Type=simple ExecStartPre=/bin/sleep 8 ExecStart=/usr/local/bin/kiosk-ptt Restart=always RestartSec=10 Environment=XDG_RUNTIME_DIR=/run/user/${KIOSK_UID} Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${KIOSK_UID}/bus Environment=KIOSK_UID=${KIOSK_UID} StandardOutput=journal StandardError=journal [Install] WantedBy=default.target EOF chown -R ${KIOSK_USER}:${KIOSK_USER} "/home/${KIOSK_USER}/.config" if [[ -n "$KIOSK_USER" ]]; then loginctl enable-linger $KIOSK_USER 2>/dev/null || true local user_dbus="XDG_RUNTIME_DIR=/run/user/${KIOSK_UID}" # Enable and start PipeWire services for the user sudo -u "$KIOSK_USER" $user_dbus systemctl --user daemon-reload sudo -u "$KIOSK_USER" $user_dbus systemctl --user enable pipewire pipewire-pulse 2>/dev/null || true sudo -u "$KIOSK_USER" $user_dbus systemctl --user restart pipewire pipewire-pulse 2>/dev/null || true # Enable baresip sudo -u "$KIOSK_USER" $user_dbus systemctl --user enable baresip # Only enable PTT if configured if [[ -f /etc/easy-asterisk/ptt-device ]]; then sudo -u "$KIOSK_USER" $user_dbus systemctl --user enable kiosk-ptt sudo -u "$KIOSK_USER" $user_dbus systemctl --user restart baresip kiosk-ptt else sudo -u "$KIOSK_USER" $user_dbus systemctl --user restart baresip # Ensure audio is unmuted for normal kiosk operation ensure_audio_unmuted fi fi } # ================================================================ # 9. CERTIFICATE HANDLING # ================================================================ check_cert_coverage() { local cert_file=$1 target_domain=$2 base_domain=$3 [[ ! -f "$cert_file" ]] && return 1 local sans=$(openssl x509 -in "$cert_file" -text -noout 2>/dev/null | grep -A1 "Subject Alternative Name" | tail -1) echo "$sans" | grep -q "DNS:${target_domain}" && return 0 echo "$sans" | grep -q "DNS:\*.${base_domain}" && return 0 return 1 } setup_caddy_cert_sync() { local mode=$1 [[ "$mode" == "force" ]] && print_header "Caddy Cert Sync" load_config local domain=${DOMAIN_NAME:-sip.example.com} if [[ "$mode" == "force" ]]; then read -p "Domain [$domain]: " input_domain domain="${input_domain:-$domain}" fi local actual_user="${SUDO_USER:-$USER}" local actual_home=$(eval echo ~"$actual_user") local base_domain=$(echo "$domain" | awk -F. '{print $(NF-1)"."$NF}') local search_paths=( "${actual_home}/docker/caddy/ssl" "${actual_home}/docker/caddy/caddy_data" "${actual_home}/docker/caddy/caddy_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory" "/var/lib/caddy" "/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory" "/data/caddy" "/root/.local/share/caddy/certificates" ) local caddy_cert="" caddy_key="" [[ "$mode" == "force" ]] && echo "Searching for certificates..." for base_path in "${search_paths[@]}"; do if ! sudo test -d "$base_path" 2>/dev/null; then continue; fi [[ "$mode" == "force" ]] && echo " Checking: $base_path" local candidates=$(sudo find "$base_path" -maxdepth 5 -type f \( -name "fullchain.pem" -o -name "*.crt" \) 2>/dev/null) for cert in $candidates; do sudo cp "$cert" /tmp/cert_check.pem 2>/dev/null || continue if check_cert_coverage "/tmp/cert_check.pem" "$domain" "$base_domain"; then [[ "$mode" == "force" ]] && print_success "Found matching cert: $cert" caddy_cert="$cert" local dir=$(dirname "$cert") local name=$(basename "$cert") if [[ "$name" == "fullchain.pem" ]]; then caddy_key="${dir}/privkey.pem" else caddy_key=$(echo "$cert" | sed 's/\.crt/\.key/') fi if sudo test -f "$caddy_key"; then rm -f /tmp/cert_check.pem break 2 fi fi rm -f /tmp/cert_check.pem done done if [[ -n "$caddy_cert" && -n "$caddy_key" ]]; then mkdir -p /etc/asterisk/certs sudo cat "$caddy_cert" > /etc/asterisk/certs/server.crt sudo cat "$caddy_key" > /etc/asterisk/certs/server.key chown asterisk:asterisk /etc/asterisk/certs/server.* chmod 644 /etc/asterisk/certs/server.crt chmod 600 /etc/asterisk/certs/server.key DOMAIN_NAME="$domain" ENABLE_TLS="y" ASTERISK_HOST="$domain" save_config generate_pjsip_conf restart_asterisk_safe [[ "$mode" == "force" ]] && print_success "Certificates installed for $domain" return 0 else [[ "$mode" == "force" ]] && print_warn "No matching certificates found" return 1 fi } setup_internet_access() { print_header "Setup Internet Access" echo "Select Certificate Source:" echo " 1) Auto-Sync from Caddy (Docker/Native)" echo " 2) Standalone Certbot (Requires Port 80 open)" echo " 3) Self-Signed (Internal testing only)" echo " 4) Manual Path" echo " 0) Cancel" read -p "Select: " cert_opt [[ "$cert_opt" == "0" ]] && return # Show port requirements show_preflight_check show_port_requirements echo "" read -p "Continue? [Y/n]: " cont [[ "$cont" =~ ^[Nn]$ ]] && return load_config read -p "FQDN [${DOMAIN_NAME:-sip.example.com}]: " fqdn DOMAIN_NAME="${fqdn:-${DOMAIN_NAME:-sip.example.com}}" ASTERISK_HOST="$DOMAIN_NAME" echo "" echo "Do you have a separate domain for TURN? (e.g., turn.example.com)" read -p "Enter TURN domain (leave empty to use $DOMAIN_NAME): " t_dom TURN_DOMAIN="${t_dom:-$DOMAIN_NAME}" # CIDR Prompt echo "" print_header "Local Network CIDR" local raw_cidr=$(ip -o -f inet addr show | awk '/scope global/ {print $4}' | head -1) local default_cidr="$raw_cidr" if [[ "$raw_cidr" =~ \.([0-9]+)/24$ ]]; then default_cidr="${raw_cidr%.*}.0/24"; fi echo "This helps Asterisk distinguish local vs external traffic." read -p "Local network CIDR [$default_cidr]: " local_net LOCAL_CIDR="${local_net:-$default_cidr}" save_config case "$cert_opt" in 1) # Caddy # Show Caddy Helper text echo "---------------------------------------------------------" echo "CADDY HELPER: Ensure these are in your Caddyfile to get certs:" echo "" echo "${DOMAIN_NAME} {" echo " respond \"Asterisk Cert Placeholder\" 200" echo "}" if [[ "$TURN_DOMAIN" != "$DOMAIN_NAME" ]]; then echo "" echo "${TURN_DOMAIN} {" echo " respond \"TURN Cert Placeholder\" 200" echo "}" fi echo "" echo "Restart Caddy, wait 30s, then press Enter." echo "---------------------------------------------------------" read -p "Press Enter to sync..." if setup_caddy_cert_sync "auto"; then print_success "Setup complete using Caddy certificates!" else print_error "Caddy sync failed. Ensure Caddy is running." return fi ;; 2) # Certbot print_info "Installing Certbot..." apt install -y certbot certbot certonly --standalone -d "$DOMAIN_NAME" --non-interactive --agree-tos --register-unsafely-without-email if [[ -f "/etc/letsencrypt/live/$DOMAIN_NAME/fullchain.pem" ]]; then mkdir -p /etc/asterisk/certs cat "/etc/letsencrypt/live/$DOMAIN_NAME/fullchain.pem" > /etc/asterisk/certs/server.crt cat "/etc/letsencrypt/live/$DOMAIN_NAME/privkey.pem" > /etc/asterisk/certs/server.key chown asterisk:asterisk /etc/asterisk/certs/server.* print_success "Certbot Success" else print_error "Certbot failed" return fi ;; 3) # Self-Signed mkdir -p /etc/asterisk/certs openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ -keyout /etc/asterisk/certs/server.key \ -out /etc/asterisk/certs/server.crt \ -subj "/CN=$DOMAIN_NAME" 2>/dev/null chown asterisk:asterisk /etc/asterisk/certs/server.* chmod 644 /etc/asterisk/certs/server.crt chmod 600 /etc/asterisk/certs/server.key print_success "Self-signed certificate generated" print_warn "Clients will need to trust this certificate" ;; 4) # Manual read -p "Certificate Path: " cp read -p "Private Key Path: " kp if [[ -f "$cp" && -f "$kp" ]]; then mkdir -p /etc/asterisk/certs cat "$cp" > /etc/asterisk/certs/server.crt cat "$kp" > /etc/asterisk/certs/server.key chown asterisk:asterisk /etc/asterisk/certs/server.* print_success "Certificates installed" else print_error "Files not found!" return fi ;; esac ENABLE_TLS="y" save_config generate_pjsip_conf restart_asterisk_safe print_success "Internet access configuration complete" } # ================================================================ # 10. INSTALLATION # ================================================================ install_full() { print_header "Full Installation" local default_user="${SUDO_USER:-$USER}" read -p "Client User [$default_user]: " target_user KIOSK_USER="${target_user:-$default_user}" KIOSK_UID=$(id -u "$KIOSK_USER") if ! collect_common_config; then return; fi collect_client_config install_dependencies INSTALLED_SERVER="y" INSTALLED_CLIENT="y" ENABLE_TLS="n" # LAN-only by default, set to "y" only if internet/certs setup is run configure_asterisk configure_baresip enable_client_services open_firewall_ports save_config echo "" echo "════════════════════════════════════════════════════════" print_success "Local network install complete" echo "" echo "Server and devices are reachable over internal LAN network only." echo "To add internet calling capability, continue with the setup below." echo "════════════════════════════════════════════════════════" echo "" read -p "Run Internet/Certificate Setup wizard now? [Y/n]: " run_setup [[ ! "$run_setup" =~ ^[Nn]$ ]] && setup_internet_access print_success "Installation complete" } install_server_only() { print_header "Server Installation" ASTERISK_HOST="127.0.0.1" ENABLE_TLS="n" # LAN-only by default, set to "y" only if internet/certs setup is run install_asterisk_packages configure_asterisk open_firewall_ports INSTALLED_SERVER="y" save_config echo "" echo "════════════════════════════════════════════════════════" print_success "Local network install complete" echo "" echo "Server and devices are reachable over internal LAN network only." echo "To add internet calling capability, continue with the setup below." echo "════════════════════════════════════════════════════════" echo "" read -p "Run Internet/Certificate Setup wizard now? [Y/n]: " run_setup [[ ! "$run_setup" =~ ^[Nn]$ ]] && setup_internet_access print_success "Server installed" } install_client_only() { print_header "Client Installation" echo "Select the user to install the kiosk client for:" echo "" if ! select_user; then print_error "User selection failed" return 1 fi echo "" read -p "Server (IP or domain): " ASTERISK_HOST read -p "SIP Password: " SIP_PASSWORD if [[ "$ASTERISK_HOST" =~ [a-zA-Z] ]]; then ENABLE_TLS="y" else ENABLE_TLS="n" fi echo "" echo "Answer Mode:" echo " 1) Auto (auto-answer incoming calls)" echo " 2) Manual (ring on incoming)" read -p "Select [1]: " aa_sel CLIENT_ANSWERMODE="auto" [[ "$aa_sel" == "2" ]] && CLIENT_ANSWERMODE="manual" collect_client_config install_baresip_packages INSTALLED_CLIENT="y" configure_baresip enable_client_services save_config print_success "Client installed" echo "" echo "════════════════════════════════════════════════════════" echo " IMPORTANT: Audio Configuration" echo "════════════════════════════════════════════════════════" echo " User: $KIOSK_USER" echo " - Audio group: Added" echo " - PipeWire services: Enabled" echo " - Microphone: Unmuted (for intercom mode)" echo "" echo " If audio doesn't work immediately:" echo " 1. Log out and log back in as '$KIOSK_USER'" echo " 2. Or reboot the system" echo " 3. Check audio with: pactl list sources short" echo "" echo " PTT Mode: Not configured (normal intercom operation)" echo " To configure PTT: Main Menu > Client Management > Configure PTT Button" echo "════════════════════════════════════════════════════════" } collect_common_config() { SIP_PASSWORD="${SIP_PASSWORD:-$(generate_password)}" ASTERISK_HOST="127.0.0.1" return 0 } collect_client_config() { read -p "Extension [101]: " KIOSK_EXTENSION KIOSK_EXTENSION="${KIOSK_EXTENSION:-101}" KIOSK_NAME="kiosk-${KIOSK_EXTENSION}" } install_dependencies() { install_asterisk_packages install_baresip_packages } install_asterisk_packages() { echo "exit 101" > /usr/sbin/policy-rc.d chmod +x /usr/sbin/policy-rc.d apt update # asterisk-opus removed (included in asterisk-modules on Ubuntu 24.04+) apt install -y asterisk asterisk-core-sounds-en-gsm asterisk-modules openssl curl tcpdump sngrep || true mkdir -p /var/lib/asterisk /var/log/asterisk /var/spool/asterisk /var/run/asterisk ldconfig update-ca-certificates 2>/dev/null || true rm -f /usr/sbin/policy-rc.d fix_asterisk_systemd } install_baresip_packages() { apt update apt install -y baresip baresip-core pipewire pipewire-alsa pipewire-pulse wireplumber alsa-utils evtest || true } uninstall_menu() { print_header "Uninstall" echo " 1) Remove Everything" echo " 2) Asterisk Only" echo " 3) Baresip Only" echo " 0) Cancel" read -p "Select: " ch case $ch in 1) systemctl stop asterisk 2>/dev/null || true apt purge -y asterisk* baresip baresip-core 2>/dev/null || true rm -rf /etc/asterisk /var/lib/asterisk /var/log/asterisk /var/spool/asterisk /usr/lib/asterisk rm -rf /etc/systemd/system/asterisk.service.d /etc/easy-asterisk [[ -n "$KIOSK_USER" ]] && rm -rf "/home/${KIOSK_USER}/.baresip" systemctl daemon-reload INSTALLED_SERVER="n" INSTALLED_CLIENT="n" rm -f "$CONFIG_FILE" print_success "Removed all" ;; 2) systemctl stop asterisk 2>/dev/null || true apt purge -y asterisk* 2>/dev/null || true rm -rf /etc/asterisk /var/lib/asterisk INSTALLED_SERVER="n" save_config print_success "Removed Asterisk" ;; 3) apt purge -y baresip baresip-core 2>/dev/null || true [[ -n "$KIOSK_USER" ]] && rm -rf "/home/${KIOSK_USER}/.baresip" INSTALLED_CLIENT="n" save_config print_success "Removed Baresip" ;; esac } # ================================================================ # 11. MENU SYSTEM (Reordered: Server #2, Devices #3) # ================================================================ show_main_menu() { clear print_header "Easy Asterisk v${SCRIPT_VERSION}" load_config echo " Status:" if [[ -f "$CONFIG_FILE" ]]; then [[ "$INSTALLED_SERVER" == "y" ]] && echo -e " Server: ${GREEN}Installed${NC}" || echo -e " Server: ${YELLOW}Not installed${NC}" [[ "$INSTALLED_CLIENT" == "y" ]] && echo -e " Client: ${GREEN}Installed${NC}" || echo -e " Client: ${YELLOW}Not installed${NC}" [[ -n "$DOMAIN_NAME" ]] && echo -e " Domain: ${DOMAIN_NAME}" else echo -e " ${YELLOW}Not configured${NC}" fi echo "" declare -A menu_map local count=1 echo " ${count}) Install/Configure"; menu_map[$count]="submenu_install"; ((count++)) if [[ "$INSTALLED_SERVER" == "y" ]]; then echo " ${count}) Server Settings"; menu_map[$count]="submenu_server"; ((count++)) echo " ${count}) Device Management"; menu_map[$count]="submenu_devices"; ((count++)) fi echo " ${count}) Client Settings"; menu_map[$count]="submenu_client"; ((count++)) echo " ${count}) Tools"; menu_map[$count]="submenu_tools"; ((count++)) echo " 0) Exit" echo "" read -p " Select: " choice [[ "$choice" == "0" ]] && exit 0 local action=${menu_map[$choice]} [[ -n "$action" ]] && $action show_main_menu } submenu_install() { clear print_header "Install" echo " 1) Full (server + client)" echo " 2) Server only" echo " 3) Client only" echo " 4) Uninstall" echo " 0) Back" read -p " Select: " choice case $choice in 1) install_full; read -p "Press Enter..." ;; 2) install_server_only; read -p "Press Enter..." ;; 3) install_client_only; read -p "Press Enter..." ;; 4) uninstall_menu; read -p "Press Enter..." ;; esac } # ================================================================ # WEB ADMIN INTERFACE # ================================================================ # WEB_ADMIN_PORT is set in load_config (default: 8080) WEB_ADMIN_SCRIPT="/usr/local/bin/easy-asterisk-webadmin" WEB_ADMIN_SERVICE="/etc/systemd/system/easy-asterisk-webadmin.service" WEB_ADMIN_HTPASSWD="/etc/easy-asterisk/webadmin.htpasswd" create_web_admin_script() { cat > "$WEB_ADMIN_SCRIPT" << 'WEBADMIN' #!/usr/bin/env python3 """ Easy Asterisk Web Admin - Simple web interface for client management """ import http.server import socketserver import json import subprocess import os import re import base64 import hashlib import html from urllib.parse import parse_qs, urlparse from functools import partial PORT = int(os.environ.get('WEBADMIN_PORT', 8080)) HTPASSWD_FILE = "/etc/easy-asterisk/webadmin.htpasswd" PJSIP_CONF = "/etc/asterisk/pjsip.conf" CATEGORIES_FILE = "/etc/easy-asterisk/categories.conf" ROOMS_FILE = "/etc/easy-asterisk/rooms.conf" CONFIG_FILE = "/etc/easy-asterisk/config" def check_auth(headers): """Verify HTTP Basic Auth against htpasswd file""" if not os.path.exists(HTPASSWD_FILE): return True # No auth required if no htpasswd file auth_header = headers.get('Authorization', '') if not auth_header.startswith('Basic '): return False try: credentials = base64.b64decode(auth_header[6:]).decode('utf-8') username, password = credentials.split(':', 1) with open(HTPASSWD_FILE, 'r') as f: for line in f: line = line.strip() if ':' in line: stored_user, stored_hash = line.split(':', 1) if stored_user == username: # Support plain text (for simplicity) or SHA256 if stored_hash.startswith('{SHA256}'): expected = '{SHA256}' + hashlib.sha256(password.encode()).hexdigest() return stored_hash == expected else: return stored_hash == password return False except: return False def get_registered_endpoints(): """Get list of registered endpoints from Asterisk - matches bash script logic""" try: # Get full endpoint details which shows Contact lines with Avail status result = subprocess.run( ['asterisk', '-rx', 'pjsip show endpoints'], capture_output=True, text=True, timeout=10 ) endpoints = {} current_endpoint = None for line in result.stdout.split('\n'): # Match endpoint header line: " Endpoint: 101/101" endpoint_match = re.match(r'\s*Endpoint:\s+(\d+)/', line) if endpoint_match: current_endpoint = endpoint_match.group(1) endpoints[current_endpoint] = 'offline' # Default to offline # Match contact line with Avail status: " Contact: 101/sip:... Avail" if current_endpoint and 'Contact:' in line: if 'Avail' in line or 'NonQual' in line: endpoints[current_endpoint] = 'online' return endpoints except: return {} def get_devices(): """Parse pjsip.conf to get device information - matches bash script logic""" devices = [] if not os.path.exists(PJSIP_CONF): return devices with open(PJSIP_CONF, 'r') as f: lines = f.readlines() dev_name = None dev_cat = None dev_aa = None for line in lines: line = line.strip() # Match device comment line if '; === Device:' in line: # Parse: ; === Device: Name (category) [AA:yes/no] === temp = line.split('; === Device:')[1] if '; === Device:' in line else '' temp = temp.split('===')[0].strip() # Remove trailing === # Check for AA tag dev_aa = None if '[AA:yes]' in temp: dev_aa = 'yes' temp = temp.replace('[AA:yes]', '').strip() elif '[AA:no]' in temp: dev_aa = 'no' temp = temp.replace('[AA:no]', '').strip() # Extract category from parentheses if '(' in temp and ')' in temp: dev_cat = temp[temp.rfind('(')+1:temp.rfind(')')] dev_name = temp[:temp.rfind('(')].strip() else: dev_name = temp dev_cat = 'unknown' # Match extension line [xxx] elif dev_name and re.match(r'^\[(\d+)\]$', line): ext = re.match(r'^\[(\d+)\]$', line).group(1) devices.append({ 'name': dev_name, 'category': dev_cat, 'extension': ext, 'auto_answer': dev_aa, 'transport': 'udp', # Default, will check below 'encryption': 'no' }) dev_name = None dev_cat = None dev_aa = None # Update transport/encryption for last added device elif devices and line.startswith('transport=transport-'): devices[-1]['transport'] = line.split('transport-')[1] elif devices and line.startswith('media_encryption='): val = line.split('=')[1] if val == 'sdes' or val == 'dtls': devices[-1]['encryption'] = val # If encryption is set but no explicit transport, assume TLS if devices[-1]['transport'] == 'udp': devices[-1]['transport'] = 'tls' elif val != 'no': devices[-1]['encryption'] = val return devices def get_categories(): """Get categories from config file""" categories = [] if os.path.exists(CATEGORIES_FILE): with open(CATEGORIES_FILE, 'r') as f: for line in f: line = line.strip() if line and not line.startswith('#'): parts = line.split('|') if len(parts) >= 3: categories.append({ 'id': parts[0], 'name': parts[1], 'auto_answer': parts[2], 'description': parts[3] if len(parts) > 3 else '' }) return categories def get_rooms(): """Get rooms from config file""" rooms = [] if os.path.exists(ROOMS_FILE): with open(ROOMS_FILE, 'r') as f: for line in f: line = line.strip() if line and not line.startswith('#'): parts = line.split('|') if len(parts) >= 5: rooms.append({ 'extension': parts[0], 'name': parts[1], 'members': parts[2], 'timeout': parts[3], 'type': parts[4] }) return rooms def delete_device(extension): """Delete a device from pjsip.conf""" if not os.path.exists(PJSIP_CONF): return False, "Config file not found" with open(PJSIP_CONF, 'r') as f: lines = f.readlines() new_lines = [] skip = False found = False pending_comment = None for line in lines: stripped = line.strip() if stripped.startswith('; === Device:'): pending_comment = line continue if re.match(rf'^\[{extension}\]$', stripped): if pending_comment: found = True skip = True pending_comment = None continue elif found: skip = True continue if pending_comment: new_lines.append(pending_comment) pending_comment = None if skip and stripped == '': skip = False continue if not skip: new_lines.append(line) if found: with open(PJSIP_CONF, 'w') as f: f.writelines(new_lines) subprocess.run(['asterisk', '-rx', 'pjsip reload'], capture_output=True) return True, "Device deleted" return False, "Device not found" def rename_device(extension, new_name): """Rename a device in pjsip.conf""" if not os.path.exists(PJSIP_CONF): return False, "Config file not found" with open(PJSIP_CONF, 'r') as f: lines = f.readlines() new_lines = [] found = False in_device = False device_ext = None for line in lines: stripped = line.strip() # Match device comment and update name if stripped.startswith('; === Device:'): # Parse the comment to get category and AA tag temp = stripped.split('; === Device:')[1].split('===')[0].strip() aa_tag = '' if '[AA:yes]' in temp: aa_tag = ' [AA:yes]' temp = temp.replace('[AA:yes]', '').strip() elif '[AA:no]' in temp: aa_tag = ' [AA:no]' temp = temp.replace('[AA:no]', '').strip() if '(' in temp: cat = temp[temp.rfind('(')+1:temp.rfind(')')] else: cat = 'unknown' # Store for next line check pending_comment = (line, cat, aa_tag) continue # Check if this is the extension we want if 'pending_comment' in dir() and pending_comment: match = re.match(r'^\[(\d+)\]$', stripped) if match and match.group(1) == extension: # This is our device - write updated comment old_line, cat, aa_tag = pending_comment new_lines.append(f'; === Device: {new_name} ({cat}){aa_tag} ===\n') new_lines.append(line) found = True in_device = True device_ext = extension pending_comment = None continue else: # Not our device, write original comment new_lines.append(pending_comment[0]) pending_comment = None # Update callerid line if in_device and stripped.startswith('callerid='): new_lines.append(f'callerid="{new_name}" <{device_ext}>\n') continue # Reset on empty line after device if in_device and stripped == '': in_device = False new_lines.append(line) if found: with open(PJSIP_CONF, 'w') as f: f.writelines(new_lines) subprocess.run(['asterisk', '-rx', 'pjsip reload'], capture_output=True) return True, "Device renamed" return False, "Device not found" def update_room_members(room_ext, new_members): """Update room members""" if not os.path.exists(ROOMS_FILE): return False, "Rooms file not found" # Read all rooms rooms = [] found = False with open(ROOMS_FILE, 'r') as f: for line in f: line = line.strip() if not line or line.startswith('#'): rooms.append(line) continue parts = line.split('|') if len(parts) >= 5 and parts[0] == room_ext: # Update this room's members parts[2] = new_members rooms.append('|'.join(parts)) found = True else: rooms.append(line) if found: with open(ROOMS_FILE, 'w') as f: f.write('\n'.join(rooms) + '\n') # Rebuild dialplan subprocess.run(['/usr/local/bin/easy-asterisk', '--rebuild-dialplan'], capture_output=True) return True, "Room members updated" return False, "Room not found" def add_device_to_room(room_ext, device_ext): """Add a device to a room""" if not os.path.exists(ROOMS_FILE): return False, "Rooms file not found" # Find the room and its current members with open(ROOMS_FILE, 'r') as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue parts = line.split('|') if len(parts) >= 5 and parts[0] == room_ext: current_members = parts[2].split(',') if parts[2] else [] # Check if device is already a member if device_ext in current_members: return False, "Device already in room" current_members.append(device_ext) new_members = ','.join(current_members) return update_room_members(room_ext, new_members) return False, "Room not found" def remove_device_from_room(room_ext, device_ext): """Remove a device from a room""" if not os.path.exists(ROOMS_FILE): return False, "Rooms file not found" # Find the room and its current members with open(ROOMS_FILE, 'r') as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue parts = line.split('|') if len(parts) >= 5 and parts[0] == room_ext: current_members = parts[2].split(',') if parts[2] else [] # Check if device is a member if device_ext not in current_members: return False, "Device not in room" current_members.remove(device_ext) new_members = ','.join(current_members) return update_room_members(room_ext, new_members) return False, "Room not found" def generate_password(length=16): """Generate a random password""" import secrets import string chars = string.ascii_letters + string.digits return ''.join(secrets.choice(chars) for _ in range(length)) def add_device(name, category, extension, conn_type='lan', auto_answer=None): """Add a new device to pjsip.conf""" if not os.path.exists(PJSIP_CONF): return False, "Config file not found" # Check if extension exists with open(PJSIP_CONF, 'r') as f: if f'[{extension}]' in f.read(): return False, "Extension already exists" password = generate_password() # Determine transport and encryption if conn_type == 'fqdn': transport = 'transport=transport-tls' encryption = 'media_encryption=sdes' ice = 'ice_support=yes' else: transport = 'transport=transport-udp' encryption = 'media_encryption=no' ice = '' aa_tag = '' if auto_answer == 'yes': aa_tag = '[AA:yes] ' elif auto_answer == 'no': aa_tag = '[AA:no] ' device_config = f''' ; === Device: {name} ({category}) {aa_tag}=== [{extension}] type=endpoint context=intercom {transport} disallow=all allow=opus allow=ulaw allow=alaw allow=g722 {encryption} direct_media=no rtp_symmetric=yes force_rport=yes rewrite_contact=yes {ice} auth={extension} aors={extension} callerid="{name}" <{extension}> [{extension}] type=auth auth_type=userpass username={extension} password={password} [{extension}] type=aor max_contacts=5 remove_existing=yes qualify_frequency=60 ''' with open(PJSIP_CONF, 'a') as f: f.write(device_config) subprocess.run(['asterisk', '-rx', 'pjsip reload'], capture_output=True) subprocess.run(['chown', 'asterisk:asterisk', PJSIP_CONF], capture_output=True) return True, {'extension': extension, 'password': password, 'name': name} def get_server_info(): """Get server configuration info""" info = { 'domain': '', 'tls_enabled': False, 'server_ip': '' } if os.path.exists(CONFIG_FILE): with open(CONFIG_FILE, 'r') as f: for line in f: if line.startswith('DOMAIN_NAME='): info['domain'] = line.split('=', 1)[1].strip().strip('"') elif line.startswith('ENABLE_TLS='): info['tls_enabled'] = 'y' in line.lower() try: result = subprocess.run(['hostname', '-I'], capture_output=True, text=True) info['server_ip'] = result.stdout.split()[0] if result.stdout else '' except: pass return info HTML_TEMPLATE = ''' Easy Asterisk - Client Admin Easy Asterisk - Client Admin Manage SIP clients and extensions Devices Rooms Categories Registered Devices ↻ + Add Device Extension Name Category Transport Status Actions Rooms (Ring/Page Groups) ↻ Extension Name Type Members Timeout Device Categories ↻ ID Name Auto-Answer Description Add New Device Configure a new SIP client Device Name Category Extension Connection Type LAN/VPN (UDP) FQDN/Internet (TLS) Auto-Answer Override Use Category Default Force Auto-Answer Force Ring Cancel Add Device Device Created Successfully Save these credentials - the password cannot be retrieved later Done Rename Device Enter a new name for extension New Name Cancel Rename ''' class WebAdminHandler(http.server.BaseHTTPRequestHandler): def log_message(self, format, *args): pass # Suppress default logging def send_auth_required(self): self.send_response(401) self.send_header('WWW-Authenticate', 'Basic realm="Easy Asterisk Admin"') self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(b'Authentication Required') def do_GET(self): if not check_auth(self.headers): self.send_auth_required() return path = urlparse(self.path).path if path == '/' or path == '/clients': self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(HTML_TEMPLATE.encode()) elif path == '/api/devices': devices = get_devices() self.send_json(devices) elif path == '/api/status': status = get_registered_endpoints() self.send_json(status) elif path == '/api/categories': categories = get_categories() self.send_json(categories) elif path == '/api/rooms': rooms = get_rooms() self.send_json(rooms) elif path == '/api/server': info = get_server_info() self.send_json(info) else: self.send_response(404) self.end_headers() def do_POST(self): if not check_auth(self.headers): self.send_auth_required() return path = urlparse(self.path).path content_length = int(self.headers.get('Content-Length', 0)) body = self.rfile.read(content_length).decode('utf-8') if path == '/api/devices': try: data = json.loads(body) success, result = add_device( data['name'], data['category'], data['extension'], data.get('conn_type', 'lan'), data.get('auto_answer') ) if success: self.send_json({'success': True, 'data': result}) else: self.send_json({'success': False, 'error': result}, 400) except Exception as e: self.send_json({'success': False, 'error': str(e)}, 400) elif path.startswith('/api/rooms/') and path.endswith('/members'): # Add device to room: POST /api/rooms/{room_ext}/members with {device: ext} room_match = re.match(r'/api/rooms/(\d+)/members', path) if room_match: try: room_ext = room_match.group(1) data = json.loads(body) device_ext = data.get('device') if not device_ext: self.send_json({'success': False, 'error': 'Device extension required'}, 400) return success, msg = add_device_to_room(room_ext, device_ext) self.send_json({'success': success, 'message': msg}) except Exception as e: self.send_json({'success': False, 'error': str(e)}, 400) else: self.send_response(404) self.end_headers() else: self.send_response(404) self.end_headers() def do_DELETE(self): if not check_auth(self.headers): self.send_auth_required() return path = urlparse(self.path).path # Delete device device_match = re.match(r'/api/devices/(\d+)$', path) if device_match: ext = device_match.group(1) success, msg = delete_device(ext) self.send_json({'success': success, 'message': msg}) return # Remove device from room: DELETE /api/rooms/{room_ext}/members/{device_ext} room_match = re.match(r'/api/rooms/(\d+)/members/(\d+)', path) if room_match: room_ext = room_match.group(1) device_ext = room_match.group(2) success, msg = remove_device_from_room(room_ext, device_ext) self.send_json({'success': success, 'message': msg}) return self.send_response(404) self.end_headers() def do_PUT(self): if not check_auth(self.headers): self.send_auth_required() return path = urlparse(self.path).path match = re.match(r'/api/devices/(\d+)', path) if match: ext = match.group(1) content_length = int(self.headers.get('Content-Length', 0)) body = self.rfile.read(content_length).decode('utf-8') try: data = json.loads(body) new_name = data.get('name', '').strip() if not new_name: self.send_json({'success': False, 'error': 'Name required'}, 400) return success, msg = rename_device(ext, new_name) self.send_json({'success': success, 'message': msg}) except Exception as e: self.send_json({'success': False, 'error': str(e)}, 400) else: self.send_response(404) self.end_headers() def send_json(self, data, status=200): self.send_response(status) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(json.dumps(data).encode()) def main(): with socketserver.TCPServer(("", PORT), WebAdminHandler) as httpd: print(f"Easy Asterisk Web Admin running on port {PORT}") httpd.serve_forever() if __name__ == "__main__": main() WEBADMIN chmod +x "$WEB_ADMIN_SCRIPT" print_success "Web admin script created" } create_web_admin_service() { cat > "$WEB_ADMIN_SERVICE" << EOF [Unit] Description=Easy Asterisk Web Admin After=network.target asterisk.service [Service] Type=simple Environment=WEBADMIN_PORT=${WEB_ADMIN_PORT} ExecStart=/usr/bin/python3 ${WEB_ADMIN_SCRIPT} Restart=always RestartSec=5 User=root [Install] WantedBy=multi-user.target EOF systemctl daemon-reload print_success "Web admin service created" } setup_web_admin_auth() { print_header "Web Admin Authentication" echo "Set up login credentials for the web admin interface." echo "" read -p "Username [admin]: " wa_user wa_user="${wa_user:-admin}" while true; do read -s -p "Password: " wa_pass echo "" if [[ ${#wa_pass} -lt 6 ]]; then print_error "Password must be at least 6 characters" continue fi read -s -p "Confirm password: " wa_pass2 echo "" if [[ "$wa_pass" != "$wa_pass2" ]]; then print_error "Passwords don't match" continue fi break done # Store with SHA256 hash local hash=$(echo -n "$wa_pass" | sha256sum | awk '{print $1}') echo "${wa_user}:{SHA256}${hash}" > "$WEB_ADMIN_HTPASSWD" chmod 600 "$WEB_ADMIN_HTPASSWD" print_success "Authentication configured for user: $wa_user" } web_admin_menu() { load_config local server_ip=$(hostname -I | awk '{print $1}') print_header "Web Admin Management" # Check current status local status="stopped" if systemctl is-active --quiet easy-asterisk-webadmin 2>/dev/null; then status="running" fi echo " Status: ${status^^}" if [[ "$status" == "running" ]]; then echo " URL: http://${server_ip}:${WEB_ADMIN_PORT}/clients" [[ -n "$DOMAIN_NAME" ]] && echo " URL: http://${DOMAIN_NAME}:${WEB_ADMIN_PORT}/clients" fi echo "" echo " 1) Start Web Admin" echo " 2) Stop Web Admin" echo " 3) Restart Web Admin" echo " 4) Configure Authentication" echo " 5) Change Port (current: ${WEB_ADMIN_PORT})" echo " 6) View Logs" echo " 0) Back" echo "" read -p " Select: " choice case $choice in 1) # Always regenerate script to ensure latest version print_info "Installing/updating web admin..." create_web_admin_script create_web_admin_service if [[ ! -f "$WEB_ADMIN_HTPASSWD" ]]; then setup_web_admin_auth fi systemctl enable easy-asterisk-webadmin systemctl start easy-asterisk-webadmin sleep 2 if systemctl is-active --quiet easy-asterisk-webadmin; then print_success "Web Admin started" echo "" echo " Access at: http://${server_ip}:${WEB_ADMIN_PORT}/clients" [[ -n "$DOMAIN_NAME" ]] && echo " Or: http://${DOMAIN_NAME}:${WEB_ADMIN_PORT}/clients" else print_error "Failed to start. Check: journalctl -u easy-asterisk-webadmin" fi ;; 2) print_info "Stopping web admin..." # First, mask the service to prevent Restart=always from respawning # This is critical - without masking, systemd will restart the process systemctl mask easy-asterisk-webadmin 2>/dev/null || true # Now stop the service if systemctl is-active --quiet easy-asterisk-webadmin 2>/dev/null; then print_info "Stopping systemd service..." systemctl stop easy-asterisk-webadmin 2>/dev/null || true sleep 1 fi # Disable the service systemctl disable easy-asterisk-webadmin 2>/dev/null || true # Kill any remaining processes on our port local port_pids=$(lsof -ti ":${WEB_ADMIN_PORT}" 2>/dev/null) if [[ -n "$port_pids" ]]; then print_info "Killing processes on port ${WEB_ADMIN_PORT}..." echo "$port_pids" | xargs kill -9 2>/dev/null || true sleep 1 fi # Unmask the service so it can be started again later systemctl unmask easy-asterisk-webadmin 2>/dev/null || true systemctl daemon-reload 2>/dev/null || true # Final verification if lsof -ti ":${WEB_ADMIN_PORT}" >/dev/null 2>&1; then print_error "Port ${WEB_ADMIN_PORT} still in use!" echo " Try manually: sudo lsof -ti :${WEB_ADMIN_PORT} | xargs sudo kill -9" else print_success "Web Admin stopped" fi ;; 3) systemctl restart easy-asterisk-webadmin print_success "Web Admin restarted" ;; 4) setup_web_admin_auth systemctl restart easy-asterisk-webadmin 2>/dev/null ;; 5) read -p "New port [${WEB_ADMIN_PORT}]: " new_port new_port="${new_port:-$WEB_ADMIN_PORT}" if [[ "$new_port" =~ ^[0-9]+$ ]] && [[ "$new_port" -ge 1024 ]] && [[ "$new_port" -le 65535 ]]; then WEB_ADMIN_PORT="$new_port" save_config create_web_admin_service systemctl restart easy-asterisk-webadmin 2>/dev/null print_success "Port changed to $new_port" else print_error "Invalid port (must be 1024-65535)" fi ;; 6) journalctl -u easy-asterisk-webadmin -n 50 --no-pager ;; 0) return ;; esac } submenu_server() { clear print_header "Server Settings" echo " 1) Setup Internet Access (TLS/Certs/NAT)" echo " 2) Force re-sync Caddy certs" echo " 3) Show port/firewall requirements" echo " 4) Interactive Firewall Guide" echo " 5) Test SIP connectivity" echo " 6) Verify CIDR/NAT config" echo " 7) Watch Live Logs" echo " 8) Router Doctor" echo " 9) Configure VLAN Subnets" echo " 10) Provisioning Manager" echo " 11) Web Admin (Client Management)" echo " 0) Back" read -p " Select: " choice case $choice in 1) setup_internet_access ;; 2) setup_caddy_cert_sync "force" ;; 3) show_port_requirements ;; 4) show_firewall_guide ;; 5) test_sip_connectivity ;; 6) verify_cidr_config ;; 7) watch_live_logs ;; 8) router_doctor ;; 9) configure_vlan_subnets ;; 10) provisioning_manager_menu ;; 11) web_admin_menu ;; 0) return ;; esac [[ "$choice" != "0" ]] && read -p "Press Enter..." [[ "$choice" != "0" ]] && submenu_server } submenu_devices() { clear print_header "Device Management" echo " 1) Add device" echo " 2) Remove device" echo " 3) Rename device" echo " 4) List devices" echo " 5) Manage categories" echo " 6) Manage rooms" echo " 7) Export Clients" echo " 8) Import Clients" echo " 0) Back" read -p " Select: " choice case $choice in 1) add_device_menu ;; 2) remove_device ;; 3) rename_device ;; 4) show_registered_devices ;; 5) manage_categories ;; 6) manage_rooms ;; 7) export_clients ;; 8) import_clients ;; 0) return ;; esac [[ "$choice" != "0" ]] && read -p "Press Enter..." [[ "$choice" != "0" ]] && submenu_devices } export_clients() { print_header "Export Client Configurations" load_config initialize_default_categories # Create export directory local timestamp=$(date +%Y%m%d_%H%M%S) local export_dir="/tmp/asterisk_export_${timestamp}" local export_file="/root/asterisk-clients-${timestamp}.tar.gz" mkdir -p "$export_dir" # Check if there are any devices to export if ! grep -q "^; === Device:" /etc/asterisk/pjsip.conf 2>/dev/null; then print_error "No client devices found to export" rm -rf "$export_dir" return fi # Export devices from pjsip.conf (everything after transport definitions) echo "Extracting client devices..." awk '/^; === Device:/{flag=1} flag' /etc/asterisk/pjsip.conf > "$export_dir/devices.conf" # Count devices local device_count=$(grep -c "^; === Device:" "$export_dir/devices.conf") # Export categories if [[ -f "$CATEGORIES_FILE" ]]; then echo "Exporting categories..." cp "$CATEGORIES_FILE" "$export_dir/categories.conf" fi # Export rooms if [[ -f "$ROOMS_FILE" ]]; then echo "Exporting rooms..." cp "$ROOMS_FILE" "$export_dir/rooms.conf" fi # Create metadata file cat > "$export_dir/export_info.txt" << EOF Easy Asterisk Client Export Export Date: $(date) Device Count: $device_count Domain: ${DOMAIN_NAME:-Not configured} TLS Enabled: ${ENABLE_TLS:-no} Exported by: $(whoami) Hostname: $(hostname) EOF # Create tar.gz archive echo "Creating archive..." tar -czf "$export_file" -C /tmp "asterisk_export_${timestamp}" 2>/dev/null # Cleanup temp directory rm -rf "$export_dir" if [[ -f "$export_file" ]]; then print_success "Export completed successfully!" echo "" echo " Exported: $device_count devices" echo " File: $export_file" echo " Size: $(du -h "$export_file" | cut -f1)" echo "" echo " To import on another system:" echo " 1) Copy file to the target server" echo " 2) Run Easy Asterisk" echo " 3) Select 'Client Settings' -> 'Import Clients'" else print_error "Export failed" fi } import_clients() { print_header "Import Client Configurations" load_config initialize_default_categories echo "Available export files in /root:" local files=($(ls -t /root/asterisk-clients-*.tar.gz 2>/dev/null)) if [[ ${#files[@]} -eq 0 ]]; then echo "" read -p "Enter full path to export file: " import_file else echo "" local i=1 for f in "${files[@]}"; do echo " $i) $(basename "$f") - $(du -h "$f" | cut -f1) - $(date -r "$f" '+%Y-%m-%d %H:%M')" ((i++)) done echo " 0) Enter custom path" echo "" read -p "Select file [1]: " file_choice file_choice="${file_choice:-1}" if [[ "$file_choice" == "0" ]]; then read -p "Enter full path to export file: " import_file elif [[ "$file_choice" -ge 1 && "$file_choice" -le ${#files[@]} ]]; then import_file="${files[$((file_choice-1))]}" else print_error "Invalid selection" return fi fi if [[ ! -f "$import_file" ]]; then print_error "File not found: $import_file" return fi # Extract to temp directory local timestamp=$(date +%Y%m%d_%H%M%S) local import_dir="/tmp/asterisk_import_${timestamp}" mkdir -p "$import_dir" echo "Extracting archive..." tar -xzf "$import_file" -C "$import_dir" 2>/dev/null # Find the extracted directory local extract_dir=$(find "$import_dir" -type d -name "asterisk_export_*" | head -1) if [[ ! -d "$extract_dir" ]]; then print_error "Invalid export file format" rm -rf "$import_dir" return fi # Show export info if [[ -f "$extract_dir/export_info.txt" ]]; then echo "" echo "═══════════════════════════════════════════════════════════════" cat "$extract_dir/export_info.txt" echo "═══════════════════════════════════════════════════════════════" echo "" fi # Count devices to import local device_count=0 if [[ -f "$extract_dir/devices.conf" ]]; then device_count=$(grep -c "^; === Device:" "$extract_dir/devices.conf") fi if [[ $device_count -eq 0 ]]; then print_error "No devices found in export file" rm -rf "$import_dir" return fi echo "This will import $device_count device(s)." echo "" read -p "Import mode [1=Merge, 2=Replace All]: " import_mode import_mode="${import_mode:-1}" if [[ "$import_mode" == "2" ]]; then echo "" echo "${RED}WARNING: This will DELETE ALL existing devices!${NC}" read -p "Type 'DELETE ALL' to confirm: " confirm if [[ "$confirm" != "DELETE ALL" ]]; then print_error "Import cancelled" rm -rf "$import_dir" return fi fi # Backup existing configurations echo "Backing up current configuration..." backup_config "/etc/asterisk/pjsip.conf" backup_config "$CATEGORIES_FILE" backup_config "$ROOMS_FILE" # Import devices if [[ "$import_mode" == "2" ]]; then # Replace mode - remove all existing devices echo "Removing existing devices..." local temp_pjsip="/tmp/pjsip_base_${timestamp}.conf" awk '/^; === Device:/{exit} {print}' /etc/asterisk/pjsip.conf > "$temp_pjsip" cat "$temp_pjsip" "$extract_dir/devices.conf" > /etc/asterisk/pjsip.conf rm -f "$temp_pjsip" print_success "Replaced all devices with imported devices" else # Merge mode - check for conflicts echo "Checking for extension conflicts..." local conflicts=0 local conflict_list="" while IFS= read -r line; do if [[ "$line" =~ ^\[([0-9]+)\]$ ]]; then local ext="${BASH_REMATCH[1]}" if grep -q "^\[${ext}\]" /etc/asterisk/pjsip.conf 2>/dev/null; then conflicts=$((conflicts + 1)) conflict_list="${conflict_list}${ext} " fi fi done < "$extract_dir/devices.conf" if [[ $conflicts -gt 0 ]]; then echo "" echo "${YELLOW}Warning: Found $conflicts conflicting extension(s): $conflict_list${NC}" read -p "Skip conflicting devices? [Y/n]: " skip_conflicts skip_conflicts="${skip_conflicts:-Y}" if [[ ! "$skip_conflicts" =~ ^[Yy]$ ]]; then print_error "Import cancelled" rm -rf "$import_dir" return fi # Import only non-conflicting devices echo "Importing non-conflicting devices..." local temp_import="/tmp/import_filtered_${timestamp}.conf" local skip_device=0 while IFS= read -r line; do if [[ "$line" == "; === Device:"* ]]; then skip_device=0 echo "$line" >> "$temp_import" elif [[ "$line" =~ ^\[([0-9]+)\]$ ]]; then local ext="${BASH_REMATCH[1]}" if grep -q "^\[${ext}\]" /etc/asterisk/pjsip.conf 2>/dev/null; then skip_device=1 echo " Skipping extension $ext (already exists)" else echo "$line" >> "$temp_import" fi elif [[ $skip_device -eq 0 ]]; then echo "$line" >> "$temp_import" fi done < "$extract_dir/devices.conf" cat "$temp_import" >> /etc/asterisk/pjsip.conf rm -f "$temp_import" else # No conflicts, import all echo "No conflicts found, importing all devices..." cat "$extract_dir/devices.conf" >> /etc/asterisk/pjsip.conf fi print_success "Devices imported successfully" fi # Import categories (merge, skip duplicates) if [[ -f "$extract_dir/categories.conf" ]]; then echo "Importing categories..." while IFS='|' read -r cat_id cat_name auto_answer description; do [[ "$cat_id" =~ ^# ]] && continue [[ -z "$cat_id" ]] && continue # Skip if already exists if grep -q "^${cat_id}|" "$CATEGORIES_FILE" 2>/dev/null; then echo " Skipping category '$cat_id' (already exists)" else echo "${cat_id}|${cat_name}|${auto_answer}|${description}" >> "$CATEGORIES_FILE" echo " Imported category: $cat_name" fi done < "$extract_dir/categories.conf" fi # Import rooms (merge, skip duplicates) if [[ -f "$extract_dir/rooms.conf" ]]; then echo "Importing rooms..." while IFS='|' read -r ext name members timeout type; do [[ "$ext" =~ ^# ]] && continue [[ -z "$ext" ]] && continue # Skip if already exists if grep -q "^${ext}|" "$ROOMS_FILE" 2>/dev/null; then echo " Skipping room '$name' (extension $ext already exists)" else echo "${ext}|${name}|${members}|${timeout}|${type}" >> "$ROOMS_FILE" echo " Imported room: $name (ext $ext)" fi done < "$extract_dir/rooms.conf" fi # Cleanup rm -rf "$import_dir" # Reload Asterisk echo "" echo "Reloading Asterisk configuration..." asterisk -rx "pjsip reload" >/dev/null 2>&1 rebuild_dialplan quiet print_success "Import completed successfully!" echo "" echo " Run 'List devices' to verify imported clients" } submenu_client() { clear print_header "Client Settings" echo " 1) Configure Local Client" echo " 2) Configure PTT Button" echo " 3) Run Diagnostics" echo " 0) Back" read -p " Select: " choice case $choice in 1) configure_local_client ;; 2) configure_ptt_menu ;; 3) run_client_diagnostics ;; 0) return ;; esac [[ "$choice" != "0" ]] && read -p "Press Enter..." [[ "$choice" != "0" ]] && submenu_client } fix_audio_manually() { print_header "Manual Audio Fix" load_config local t_user="${KIOSK_USER:-$SUDO_USER}" t_user="${t_user:-$USER}" local t_uid=$(id -u "$t_user" 2>/dev/null) local user_dbus="XDG_RUNTIME_DIR=/run/user/$t_uid" echo "Fixing audio for user: $t_user" echo "" # Restart PipeWire services echo "Restarting PipeWire services..." sudo -u "$t_user" $user_dbus systemctl --user restart pipewire pipewire-pulse 2>/dev/null || true sleep 2 # Unmute audio echo "Unmuting audio sources and sinks..." sudo -u "$t_user" $user_dbus pactl set-source-mute @DEFAULT_SOURCE@ 0 2>/dev/null && echo " ✓ Microphone unmuted" || echo " ✗ Failed to unmute microphone" sudo -u "$t_user" $user_dbus pactl set-sink-mute @DEFAULT_SINK@ 0 2>/dev/null && echo " ✓ Speaker unmuted" || echo " ✗ Failed to unmute speaker" # Set volume echo "Setting volume levels to 75%..." sudo -u "$t_user" $user_dbus pactl set-source-volume @DEFAULT_SOURCE@ 75% 2>/dev/null && echo " ✓ Microphone volume set" || echo " ✗ Failed to set microphone volume" sudo -u "$t_user" $user_dbus pactl set-sink-volume @DEFAULT_SINK@ 75% 2>/dev/null && echo " ✓ Speaker volume set" || echo " ✗ Failed to set speaker volume" echo "" echo "Current audio status:" local src_mute=$(sudo -u "$t_user" $user_dbus pactl get-source-mute @DEFAULT_SOURCE@ 2>/dev/null | awk '{print $2}') local sink_mute=$(sudo -u "$t_user" $user_dbus pactl get-sink-mute @DEFAULT_SINK@ 2>/dev/null | awk '{print $2}') echo " Microphone: ${src_mute:-unknown}" echo " Speaker: ${sink_mute:-unknown}" echo "" echo "Restarting Baresip..." sudo -u "$t_user" $user_dbus systemctl --user restart baresip 2>/dev/null && echo " ✓ Baresip restarted" || echo " ✗ Failed to restart Baresip" } submenu_tools() { clear print_header "Tools" echo " 1) Audio Test" echo " 2) Verify Audio/Codec Setup" echo " 3) Fix Audio (Unmute & Restart)" echo " 4) Room Directory" echo " 5) Manual Update Asterisk" echo " 0) Back" read -p " Select: " choice case $choice in 1) run_audio_test ;; 2) verify_audio_setup ;; 3) fix_audio_manually ;; 4) show_room_directory ;; 5) manual_update_asterisk ;; 0) return ;; esac [[ "$choice" != "0" ]] && read -p "Press Enter..." [[ "$choice" != "0" ]] && submenu_tools } main() { check_root load_config show_main_menu } main "$@"
Manage SIP clients and extensions
Configure a new SIP client
Save these credentials - the password cannot be retrieved later
Enter a new name for extension