Files
easy-asterisk/easy-asterisk-v0.9.8.7.sh
T
Claude 26cec84c56 Add VLAN configuration support to prevent call drops
This update adds comprehensive VLAN support to prevent the common
issue of calls dropping after 30 seconds in multi-VLAN environments.

Changes:
- Added new menu item "Configure VLAN Subnets" in Server Settings
- Created configure_vlan_subnets() function to manage VLAN configuration
- Updated pjsip.conf generation to include multiple local_net entries
- Added server IP address as comment in transport sections for reference
- Persist VLAN settings (HAS_VLANS, VLAN_SUBNETS) in config file
- Updated script version to v0.9.8.8

Technical details:
- The generate_pjsip_conf() function now builds multiple local_net
  entries when VLANs are configured
- Each VLAN subnet is added as a separate local_net line in the
  transport configuration
- Server IP is detected and documented in transport sections
- Transports continue to bind to 0.0.0.0 for all-interface listening

This fixes the issue where Asterisk would treat devices on different
VLANs as external connections, causing NAT issues and call drops.
2025-12-08 20:17:48 +00:00

3427 lines
118 KiB
Bash

#!/bin/bash
# ================================================================
# Easy Asterisk - Interactive Installer v0.9.8
#
# UPDATES in v0.9.8:
# - FIXED: Categories not displaying correctly in lists of devices
# - ADDED: Client export/import functionality in Device Management menu
# - FEATURE: ADDED: Ability to rename categories and rooms
# - FEATURE: Export all provisioned clients to tar.gz archive
# - FEATURE: Import clients with merge or replace modes
# - FEATURE: Automatic conflict detection and resolution during import
# - FEATURE: Backup protection before import operations
#
# RETAINED from v0.9.5:
# - FIXED: Stasis disabled causing startup failure (now only disables optional modules)
# - ADDED: Automatic VLAN/NAT traversal (works with flat, VPN, and VLAN networks)
# - ADDED: Per-device connection type selection (LAN/VPN vs FQDN)
# - IMPROVED: Transport-level NAT configuration for better VLAN support
# - IMPROVED: Generic router references (not brand-specific)
# - COMPATIBLE: Works with flat networks, VLANs, VPNs, and FQDN modes
# ================================================================
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"
DEFAULT_TURN_PORT="3478"
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"
COTURN_CONFIG="/etc/turnserver.conf"
SCRIPT_VERSION="0.9.8.8"
# ================================================================
# 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}"
INSTALLED_COTURN="${INSTALLED_COTURN:-n}"
KIOSK_USER="${KIOSK_USER:-}"
KIOSK_UID="${KIOSK_UID:-}"
USE_COTURN="${USE_COTURN:-n}"
TURN_SECRET="${TURN_SECRET:-}"
TURN_USER="${TURN_USER:-kioskuser}"
TURN_PASS="${TURN_PASS:-}"
TURN_DOMAIN="${TURN_DOMAIN:-}"
HAS_VLANS="${HAS_VLANS:-n}"
VLAN_SUBNETS="${VLAN_SUBNETS:-}"
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"
TURN_DOMAIN="$TURN_DOMAIN"
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"
INSTALLED_COTURN="$INSTALLED_COTURN"
USE_COTURN="$USE_COTURN"
TURN_SECRET="$TURN_SECRET"
TURN_USER="$TURN_USER"
TURN_PASS="$TURN_PASS"
CURRENT_PUBLIC_IP="$CURRENT_PUBLIC_IP"
PTT_DEVICE="$PTT_DEVICE"
PTT_KEYCODE="$PTT_KEYCODE"
LOCAL_CIDR="$LOCAL_CIDR"
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
if [[ "$USE_COTURN" == "y" ]]; then
ufw allow ${DEFAULT_TURN_PORT}/udp comment "TURN UDP" 2>/dev/null || true
ufw allow ${DEFAULT_TURN_PORT}/tcp comment "TURN TCP" 2>/dev/null || true
# Allow relay range for TURN
ufw allow 49152:65535/udp comment "TURN Relay" 2>/dev/null || true
fi
ufw reload 2>/dev/null || true
print_success "UFW firewall ports opened"
fi
fi
}
# ================================================================
# 2. COTURN SETUP & DYNAMIC IP
# ================================================================
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"
}
install_coturn() {
print_header "Installing COTURN"
apt update
apt install -y coturn
if [[ -z "$TURN_PASS" ]]; then
TURN_PASS=$(generate_password)
fi
local public_ip=$(get_public_ip)
CURRENT_PUBLIC_IP="$public_ip"
if [[ -z "$public_ip" ]]; then
print_error "Could not detect public IP"
return 1
fi
print_info "Public IP: $public_ip"
# Enable coturn
sed -i 's/#TURNSERVER_ENABLED=1/TURNSERVER_ENABLED=1/' /etc/default/coturn 2>/dev/null || true
# Configure coturn
backup_config "$COTURN_CONFIG"
cat > "$COTURN_CONFIG" << EOF
# Easy Asterisk COTURN Configuration
listening-port=${DEFAULT_TURN_PORT}
fingerprint
lt-cred-mech
realm=${TURN_DOMAIN:-${DOMAIN_NAME:-turn.local}}
total-quota=100
stale-nonce=600
cert=/etc/asterisk/certs/server.crt
pkey=/etc/asterisk/certs/server.key
no-tlsv1
no-tlsv1_1
cipher-list="ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384"
dh2066
no-stdout-log
log-file=/var/log/turnserver.log
simple-log
external-ip=${public_ip}
min-port=49152
max-port=65535
user-quota=12
no-multicast-peers
no-cli
user=${TURN_USER}:${TURN_PASS}
EOF
chmod 600 "$COTURN_CONFIG"
systemctl enable coturn
systemctl restart coturn
if systemctl is-active coturn >/dev/null; then
print_success "COTURN installed and running"
INSTALLED_COTURN="y"
USE_COTURN="y"
save_config
return 0
else
print_error "COTURN failed to start"
journalctl -u coturn -n 20 --no-pager
return 1
fi
}
configure_coturn_credentials() {
print_header "Configure TURN Credentials"
echo "Current User: ${TURN_USER}"
echo "Current Pass: ${TURN_PASS}"
echo ""
read -p "Enter Username [${TURN_USER}]: " t_user
t_user="${t_user:-$TURN_USER}"
read -p "Enter Password [generate]: " t_pass
t_pass="${t_pass:-$(generate_password)}"
TURN_USER="$t_user"
TURN_PASS="$t_pass"
if [[ -f "$COTURN_CONFIG" ]]; then
# Remove old user lines and add new one
sed -i '/^user=/d' "$COTURN_CONFIG"
echo "user=${TURN_USER}:${TURN_PASS}" >> "$COTURN_CONFIG"
systemctl restart coturn
print_success "Credentials updated and service restarted"
else
print_error "COTURN not installed. Run install first."
fi
save_config
}
update_coturn_ip() {
local new_ip=$(get_public_ip)
if [[ -z "$new_ip" ]]; then
print_warn "Could not detect public IP"
return 1
fi
if [[ "$new_ip" == "$CURRENT_PUBLIC_IP" ]]; then
print_info "IP unchanged: $new_ip"
return 0
fi
print_info "IP changed: $CURRENT_PUBLIC_IP -> $new_ip"
# Update coturn config
if [[ -f "$COTURN_CONFIG" ]]; then
sed -i "s/^external-ip=.*/external-ip=${new_ip}/" "$COTURN_CONFIG"
systemctl restart coturn
print_success "COTURN updated"
fi
# Update pjsip config
if [[ -f /etc/asterisk/pjsip.conf ]]; then
sed -i "s/^external_media_address=.*/external_media_address=${new_ip}/" /etc/asterisk/pjsip.conf
sed -i "s/^external_signaling_address=.*/external_signaling_address=${new_ip}/" /etc/asterisk/pjsip.conf
asterisk -rx "pjsip reload" 2>/dev/null
print_success "Asterisk updated"
fi
CURRENT_PUBLIC_IP="$new_ip"
save_config
return 0
}
create_ip_update_script() {
cat > /usr/local/bin/easy-asterisk-update-ip << 'IPSCRIPT'
#!/bin/bash
CONFIG_FILE="/etc/easy-asterisk/config"
[[ -f "$CONFIG_FILE" ]] && source "$CONFIG_FILE"
get_public_ip() {
curl -s -4 --connect-timeout 5 ifconfig.me 2>/dev/null || curl -s -4 --connect-timeout 5 icanhazip.com 2>/dev/null
}
NEW_IP=$(get_public_ip)
[[ -z "$NEW_IP" ]] && exit 0
[[ "$NEW_IP" == "$CURRENT_PUBLIC_IP" ]] && exit 0
# Update COTURN
if [[ -f /etc/turnserver.conf ]]; then
sed -i "s/^external-ip=.*/external-ip=${NEW_IP}/" /etc/turnserver.conf
systemctl restart coturn
fi
# Update Asterisk
if [[ -f /etc/asterisk/pjsip.conf ]]; then
sed -i "s/^external_media_address=.*/external_media_address=${NEW_IP}/" /etc/asterisk/pjsip.conf
sed -i "s/^external_signaling_address=.*/external_signaling_address=${NEW_IP}/" /etc/asterisk/pjsip.conf
asterisk -rx "pjsip reload" 2>/dev/null
fi
# Update config file
sed -i "s/^CURRENT_PUBLIC_IP=.*/CURRENT_PUBLIC_IP=\"${NEW_IP}\"/" "$CONFIG_FILE"
logger "Easy Asterisk: Updated IP to ${NEW_IP}"
IPSCRIPT
chmod +x /usr/local/bin/easy-asterisk-update-ip
# Create systemd timer
cat > /etc/systemd/system/easy-asterisk-ip-update.service << 'EOF'
[Unit]
Description=Easy Asterisk IP Update
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/easy-asterisk-update-ip
EOF
cat > /etc/systemd/system/easy-asterisk-ip-update.timer << 'EOF'
[Unit]
Description=Easy Asterisk IP Update Timer
[Timer]
OnBootSec=2min
OnUnitActiveSec=10min
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable easy-asterisk-ip-update.timer
systemctl start easy-asterisk-ip-update.timer
print_success "IP update timer installed (checks every 10 minutes)"
}
configure_coturn_menu() {
print_header "Configure COTURN (TURN Server)"
if [[ "$INSTALLED_COTURN" == "y" ]]; then
echo -e "Status: ${GREEN}Installed${NC}"
echo "User: ${TURN_USER}"
echo "Pass: ${TURN_PASS}"
echo ""
echo " 1) Update Credentials (User/Pass)"
echo " 2) Reinstall/Reconfigure"
echo " 3) Update IP manually"
echo " 4) Show configuration"
echo " 5) Uninstall"
echo " 0) Back"
read -p "Select: " choice
case $choice in
1) configure_coturn_credentials ;;
2) install_coturn ;;
3) update_coturn_ip ;;
4) show_coturn_config ;;
5) uninstall_coturn ;;
esac
else
echo "COTURN is not installed."
read -p "Install now? [Y/n]: " install
if [[ ! "$install" =~ ^[Nn]$ ]]; then
install_coturn
if [[ "$INSTALLED_COTURN" == "y" ]]; then
create_ip_update_script
fi
fi
fi
}
show_coturn_config() {
print_header "COTURN Configuration"
echo "Status: $(systemctl is-active coturn)"
echo "Public IP: $CURRENT_PUBLIC_IP"
echo "Port: ${DEFAULT_TURN_PORT}"
echo "Credentials: ${TURN_USER} : ${TURN_PASS}"
echo ""
echo "Client Config String:"
echo "turn:${TURN_USER}:${TURN_PASS}@${TURN_DOMAIN:-${DOMAIN_NAME:-$CURRENT_PUBLIC_IP}}:${DEFAULT_TURN_PORT}"
echo ""
echo "Logs:"
tail -n 20 /var/log/turnserver.log 2>/dev/null || echo " No logs found"
}
uninstall_coturn() {
systemctl stop coturn 2>/dev/null || true
systemctl disable coturn 2>/dev/null || true
apt purge -y coturn 2>/dev/null || true
rm -f /etc/turnserver.conf
rm -f /usr/local/bin/easy-asterisk-update-ip
systemctl stop easy-asterisk-ip-update.timer 2>/dev/null || true
systemctl disable easy-asterisk-ip-update.timer 2>/dev/null || true
rm -f /etc/systemd/system/easy-asterisk-ip-update.*
systemctl daemon-reload
INSTALLED_COTURN="n"
USE_COTURN="n"
save_config
print_success "COTURN uninstalled"
}
# ================================================================
# 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
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
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
echo ""
echo "═══════════════════════════════════════════════════════════════"
echo " DEVICE ADDED"
echo "═══════════════════════════════════════════════════════════════"
echo ""
echo -e " ${BOLD}Client Configuration:${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 "═══════════════════════════════════════════════════════════════"
}
remove_device() {
print_header "Remove Device"
declare -A REMOVE_MAP
local count=1
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:*\]}"
local name="${temp% (*)}"
fi
if [[ "$line" =~ ^\[([0-9]+)\] ]]; then
local ext="${BASH_REMATCH[1]}"
if [[ -n "$name" ]]; then
echo " ${count}) Ext ${ext} - ${name}"
REMOVE_MAP[$count]=$ext
((count++))
name=""
fi
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
sed -i '/^; === Device:/,/^$/d' /etc/asterisk/pjsip.conf
sed -i '/^\[[0-9]\{3\}\]/,/^$/d' /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]}"
read -p "Confirm removal of $ext? [y/N]: " confirm
if [[ "$confirm" =~ ^[Yy]$ ]]; then
backup_config "/etc/asterisk/pjsip.conf"
sed -i "/^; === Device:.*${ext}.*/,/^$/d" /etc/asterisk/pjsip.conf
sed -i "/^\[${ext}\]/,/^$/d" /etc/asterisk/pjsip.conf
asterisk -rx "pjsip reload" 2>/dev/null
rebuild_dialplan
print_success "Removed"
fi
}
rename_device() {
print_header "Rename Device"
declare -A DEVICE_MAP
declare -A NAME_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% ===}"
temp="${temp% \[AA:*\]}"
local name="${temp% (*)}"
local cat="${temp##* (}"; cat="${cat%)}"
fi
if [[ "$line" =~ ^\[([0-9]+)\] ]]; then
local ext="${BASH_REMATCH[1]}"
if [[ -n "$name" ]]; then
echo " ${count}) Ext ${ext} - ${name} (${cat})"
DEVICE_MAP[$count]=$ext
NAME_MAP[$count]="${name}|${cat}"
((count++))
name=""
fi
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 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"
# Update the device comment line
sed -i "s/^; === Device: ${old_name} (${cat})/; === Device: ${new_name} (${cat})/" /etc/asterisk/pjsip.conf
# Update the callerid line
sed -i "/^\[${ext}\]/,/^$/ s/callerid=\"${old_name}\"/callerid=\"${new_name}\"/" /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"
else
print_error "No subnets provided"
fi
else
HAS_VLANS="n"
VLAN_SUBNETS=""
save_config
print_success "VLAN support disabled"
fi
}
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
<sip:${ext}@${server};transport=${transport_str}>;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 v1.23
[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
<sip:${KIOSK_EXTENSION}@${ASTERISK_HOST};transport=${transport}>;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 v1.23"
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}"
[[ "$INSTALLED_COTURN" == "y" ]] && echo -e " COTURN: ${GREEN}Installed${NC}" || echo -e " COTURN: ${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
}
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 TURN Server (COTURN)"
echo " 10) Configure VLAN Subnets"
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_coturn_menu ;;
10) configure_vlan_subnets ;;
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 " 0) Back"
read -p " Select: " choice
case $choice in
1) run_audio_test ;;
2) verify_audio_setup ;;
3) fix_audio_manually ;;
0) return ;;
esac
[[ "$choice" != "0" ]] && read -p "Press Enter..."
[[ "$choice" != "0" ]] && submenu_tools
}
main() {
check_root
load_config
show_main_menu
}
main "$@"