#!/bin/sh # voipctl - SecuBox VoIP PBX Control Script # Manages Asterisk PBX in LXC container with OVH SIP trunk set -e CONTAINER_NAME="voip" CONTAINER_PATH="/srv/lxc/$CONTAINER_NAME" DATA_PATH="/srv/voip" LIB_PATH="/usr/lib/secubox/voip" # Source helper libraries [ -f "$LIB_PATH/ovh-telephony.sh" ] && . "$LIB_PATH/ovh-telephony.sh" [ -f "$LIB_PATH/asterisk-config.sh" ] && . "$LIB_PATH/asterisk-config.sh" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } log_err() { echo -e "${RED}[ERROR]${NC} $*" >&2; } usage() { cat < [options] Container Management: install Create LXC container and install Asterisk uninstall Remove container and all data start Start VoIP services stop Stop VoIP services restart Restart VoIP services status Show status (JSON) logs [-f] View Asterisk logs shell Open shell in container Extension Management: ext add [password] Add extension ext del Delete extension ext list List extensions ext passwd [password] Set extension password Trunk Management: trunk add ovh Auto-provision OVH SIP trunk trunk add manual Configure trunk manually trunk test Test trunk connectivity trunk status Show registration status Call Operations: call Originate call (click-to-call) hangup Hang up active call calls List active calls Voicemail: vm list [ext] List voicemails vm play Play voicemail vm delete Delete voicemail Call Recording: rec enable Enable call recording rec disable Disable call recording rec status Show recording status rec list [date] List recordings (YYYYMMDD) rec play Play recording rec download Get download path rec delete Delete recording rec cleanup [days] Delete recordings older than N days Configuration: configure-haproxy Setup WebRTC proxy in HAProxy emancipate Full exposure with SSL reload Reload Asterisk configuration EOF exit 1 } # Check if container exists container_exists() { [ -d "$CONTAINER_PATH/rootfs" ] } # Check if container is running container_running() { lxc-info -n "$CONTAINER_NAME" -s 2>/dev/null | grep -q "RUNNING" } # Execute command in container container_exec() { if ! container_running; then log_err "Container not running" return 1 fi lxc-attach -n "$CONTAINER_NAME" -- "$@" } # Generate random password gen_password() { head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 16 } # # Container Installation # cmd_install() { if container_exists; then log_warn "Container already exists. Use 'uninstall' first to reinstall." return 1 fi log_info "Creating VoIP container..." mkdir -p "$CONTAINER_PATH" "$DATA_PATH" # Create LXC config cat > "$CONTAINER_PATH/config" </dev/null 2>&1; then debootstrap --arch="$arch" --variant=minbase "$release" "$CONTAINER_PATH/rootfs" "$mirror" else # Download pre-built rootfs local rootfs_url="https://images.linuxcontainers.org/images/debian/$release/$arch/default/" local latest=$(wget -qO- "$rootfs_url" | grep -oE '[0-9]{8}_[0-9]{2}:[0-9]{2}' | tail -1) wget -O /tmp/rootfs.tar.xz "${rootfs_url}${latest}/rootfs.tar.xz" mkdir -p "$CONTAINER_PATH/rootfs" tar -xJf /tmp/rootfs.tar.xz -C "$CONTAINER_PATH/rootfs" rm -f /tmp/rootfs.tar.xz fi log_info "Installing Asterisk packages..." # Setup resolv.conf cp /etc/resolv.conf "$CONTAINER_PATH/rootfs/etc/resolv.conf" # Install packages cat > "$CONTAINER_PATH/rootfs/tmp/setup.sh" <<'SETUP' #!/bin/bash export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y --no-install-recommends \ asterisk \ asterisk-core-sounds-en \ asterisk-moh-opsound-wav \ asterisk-modules \ tini \ curl \ jq \ ca-certificates # Clean up apt-get clean rm -rf /var/lib/apt/lists/* # Create voip data directory mkdir -p /srv/voip/{sounds,recordings,voicemail,logs} SETUP chmod +x "$CONTAINER_PATH/rootfs/tmp/setup.sh" # Start container temporarily to run setup lxc-start -n "$CONTAINER_NAME" -d -F -- /bin/bash /tmp/setup.sh # Wait for setup to complete local timeout=300 while [ $timeout -gt 0 ] && lxc-info -n "$CONTAINER_NAME" -s 2>/dev/null | grep -q "RUNNING"; do sleep 5 timeout=$((timeout - 5)) done # Create startup script cat > "$CONTAINER_PATH/rootfs/start-voip.sh" <<'STARTUP' #!/bin/bash # VoIP container startup script # Clean stale PID files rm -f /var/run/asterisk/asterisk.pid 2>/dev/null # Start Asterisk /usr/sbin/asterisk -f -vvvg -c STARTUP chmod +x "$CONTAINER_PATH/rootfs/start-voip.sh" # Generate initial Asterisk config generate_asterisk_config log_ok "VoIP container installed successfully" log_info "Run 'voipctl start' to start services" } cmd_uninstall() { log_warn "This will remove the VoIP container and all data!" echo -n "Continue? [y/N] " read -r confirm [ "$confirm" = "y" ] || [ "$confirm" = "Y" ] || return 1 if container_running; then log_info "Stopping container..." lxc-stop -n "$CONTAINER_NAME" -k 2>/dev/null || true fi log_info "Removing container..." rm -rf "$CONTAINER_PATH" log_ok "Container removed" log_info "Data directory preserved at $DATA_PATH" } cmd_start() { if ! container_exists; then log_err "Container not installed. Run 'voipctl install' first." return 1 fi if container_running; then log_warn "Container already running" return 0 fi log_info "Starting VoIP container..." lxc-start -n "$CONTAINER_NAME" -d # Wait for Asterisk to be ready local timeout=30 while [ $timeout -gt 0 ]; do if container_exec asterisk -rx "core show version" >/dev/null 2>&1; then log_ok "Asterisk started" return 0 fi sleep 1 timeout=$((timeout - 1)) done log_err "Asterisk failed to start in time" return 1 } cmd_stop() { if ! container_running; then log_warn "Container not running" return 0 fi log_info "Stopping VoIP container..." lxc-stop -n "$CONTAINER_NAME" log_ok "Container stopped" } cmd_restart() { cmd_stop sleep 2 cmd_start } cmd_status() { local running=0 local registered=0 local active_calls=0 local extensions=0 if container_running; then running=1 # Check trunk registration if container_exec asterisk -rx "pjsip show registrations" 2>/dev/null | grep -q "Registered"; then registered=1 fi # Count active calls local calls_output calls_output=$(container_exec asterisk -rx "core show channels" 2>/dev/null | grep -oE "^[0-9]+ active" | head -1 | cut -d' ' -f1) || true active_calls=${calls_output:-0} # Count extensions local ext_output ext_output=$(container_exec asterisk -rx "pjsip show endpoints" 2>/dev/null | wc -l) || true extensions=${ext_output:-0} fi cat < [password]" return 1 } # Validate extension number if ! echo "$num" | grep -qE '^[0-9]{3,6}$'; then log_err "Extension must be 3-6 digits" return 1 fi # Add to UCI local section="ext_$num" uci set voip.$section=extension uci set voip.$section.name="$name" uci set voip.$section.secret="$secret" uci set voip.$section.context="internal" uci set voip.$section.voicemail="1" uci commit voip # Regenerate Asterisk config generate_pjsip_extensions log_ok "Extension $num created for $name" echo "Password: $secret" } cmd_ext_del() { local num="$1" [ -z "$num" ] && { log_err "Usage: voipctl ext del " return 1 } uci delete "voip.ext_$num" 2>/dev/null || { log_err "Extension $num not found" return 1 } uci commit voip generate_pjsip_extensions log_ok "Extension $num deleted" } cmd_ext_list() { echo "Extensions:" echo "----------" uci show voip 2>/dev/null | grep "=extension" | while read -r line; do local section=$(echo "$line" | cut -d'.' -f2 | cut -d'=' -f1) local num=$(echo "$section" | sed 's/ext_//') local name=$(uci -q get "voip.$section.name") echo " $num: $name" done } cmd_ext_passwd() { local num="$1" local secret="${2:-$(gen_password)}" [ -z "$num" ] && { log_err "Usage: voipctl ext passwd [password]" return 1 } uci set "voip.ext_$num.secret=$secret" 2>/dev/null || { log_err "Extension $num not found" return 1 } uci commit voip generate_pjsip_extensions log_ok "Password updated for extension $num" echo "New password: $secret" } # # Trunk Management # cmd_trunk_add_ovh() { log_info "Provisioning OVH SIP trunk..." # Check OVH credentials local app_key=$(uci -q get voip.ovh_telephony.app_key) local app_secret=$(uci -q get voip.ovh_telephony.app_secret) local consumer_key=$(uci -q get voip.ovh_telephony.consumer_key) if [ -z "$app_key" ] || [ -z "$app_secret" ] || [ -z "$consumer_key" ]; then log_err "OVH API credentials not configured" log_info "Set credentials in /etc/config/voip under ovh_telephony section" return 1 fi # Fetch billing accounts log_info "Fetching OVH telephony accounts..." local accounts=$(ovh_api_get "/telephony") if [ -z "$accounts" ] || [ "$accounts" = "[]" ]; then log_err "No telephony accounts found" return 1 fi echo "Available billing accounts:" echo "$accounts" | jsonfilter -e '@[*]' | nl -w2 -s'. ' echo -n "Select account number: " read -r account_num local billing_account=$(echo "$accounts" | jsonfilter -e "@[$((account_num-1))]") # Fetch SIP lines log_info "Fetching SIP lines..." local lines=$(ovh_api_get "/telephony/$billing_account/line") echo "Available SIP lines:" echo "$lines" | jsonfilter -e '@[*]' | nl -w2 -s'. ' echo -n "Select line number: " read -r line_num local service_name=$(echo "$lines" | jsonfilter -e "@[$((line_num-1))]") # Get SIP credentials log_info "Fetching SIP credentials..." local sip_accounts=$(ovh_api_get "/telephony/$billing_account/line/$service_name/sipAccounts") local sip_username=$(echo "$sip_accounts" | jsonfilter -e '@[0]') # Save to UCI uci set voip.ovh_telephony.billing_account="$billing_account" uci set voip.ovh_telephony.service_name="$service_name" uci set voip.sip_trunk.enabled="1" uci set voip.sip_trunk.provider="ovh" uci set voip.sip_trunk.host="sip.ovh.net" uci set voip.sip_trunk.username="$sip_username" uci commit voip log_info "Enter SIP password (from OVH manager):" read -rs sip_password uci set voip.sip_trunk.password="$sip_password" uci commit voip # Generate PJSIP config generate_pjsip_trunk log_ok "OVH trunk configured: $sip_username" log_info "Run 'voipctl restart' to apply changes" } cmd_trunk_test() { if ! container_running; then log_err "Container not running" return 1 fi log_info "Testing trunk registration..." container_exec asterisk -rx "pjsip show registrations" } cmd_trunk_status() { if ! container_running; then log_err "Container not running" return 1 fi container_exec asterisk -rx "pjsip show registrations" echo container_exec asterisk -rx "pjsip show endpoints" | head -20 } # # Call Operations # cmd_call() { local from="$1" local to="$2" [ -z "$from" ] || [ -z "$to" ] && { log_err "Usage: voipctl call " return 1 } if ! container_running; then log_err "Container not running" return 1 fi log_info "Originating call: $from -> $to" # First ring the extension, then connect to destination local channel=$(container_exec asterisk -rx "channel originate PJSIP/$from application Dial PJSIP/$to@ovh-trunk" 2>&1) if echo "$channel" | grep -qi "error"; then log_err "Failed to originate call: $channel" return 1 fi log_ok "Call initiated" echo "$channel" } cmd_hangup() { local channel="$1" [ -z "$channel" ] && { log_err "Usage: voipctl hangup " return 1 } container_exec asterisk -rx "channel request hangup $channel" log_ok "Hangup requested" } cmd_calls() { if ! container_running; then log_err "Container not running" return 1 fi container_exec asterisk -rx "core show channels" } # # Voicemail # cmd_vm_list() { local ext="$1" if [ -n "$ext" ]; then find "$DATA_PATH/voicemail/default/$ext" -name "msg*.wav" 2>/dev/null | while read -r msg; do local id=$(basename "$msg" .wav) local info=$(cat "${msg%.wav}.txt" 2>/dev/null | grep -E "^(callerid|origdate)=" | tr '\n' ' ') echo "$id: $info" done else find "$DATA_PATH/voicemail/default" -type d -name "[0-9]*" 2>/dev/null | while read -r dir; do local ext=$(basename "$dir") local count=$(find "$dir" -name "msg*.wav" 2>/dev/null | wc -l) echo "$ext: $count messages" done fi } cmd_vm_play() { local ext="$1" local id="$2" [ -z "$ext" ] || [ -z "$id" ] && { log_err "Usage: voipctl vm play " return 1 } local file="$DATA_PATH/voicemail/default/$ext/$id.wav" if [ -f "$file" ]; then # Play via aplay or just output path if command -v aplay >/dev/null; then aplay "$file" else echo "Voicemail file: $file" fi else log_err "Message not found: $id" return 1 fi } cmd_vm_delete() { local ext="$1" local id="$2" [ -z "$ext" ] || [ -z "$id" ] && { log_err "Usage: voipctl vm delete " return 1 } rm -f "$DATA_PATH/voicemail/default/$ext/$id".* log_ok "Message deleted" } # # Call Recording # RECORDINGS_PATH="$DATA_PATH/recordings" cmd_rec_enable() { log_info "Enabling call recording..." uci set voip.recording=recording uci set voip.recording.enabled="1" uci set voip.recording.format="wav" uci set voip.recording.retention_days="30" uci commit voip # Create recordings directory mkdir -p "$RECORDINGS_PATH" # Regenerate dialplan with recording enabled generate_dialplan if container_running; then container_exec asterisk -rx "dialplan reload" fi log_ok "Call recording enabled" log_info "Recordings will be saved to: $RECORDINGS_PATH" } cmd_rec_disable() { log_info "Disabling call recording..." uci set voip.recording.enabled="0" uci commit voip generate_dialplan if container_running; then container_exec asterisk -rx "dialplan reload" fi log_ok "Call recording disabled" } cmd_rec_status() { local enabled=$(uci -q get voip.recording.enabled) local format=$(uci -q get voip.recording.format || echo "wav") local retention=$(uci -q get voip.recording.retention_days || echo "30") local total_count=0 local total_size=0 local today_count=0 if [ -d "$RECORDINGS_PATH" ]; then total_count=$(find "$RECORDINGS_PATH" -type f -name "*.$format" 2>/dev/null | wc -l) total_size=$(du -sh "$RECORDINGS_PATH" 2>/dev/null | cut -f1 || echo "0") local today=$(date +%Y%m%d) if [ -d "$RECORDINGS_PATH/$today" ]; then today_count=$(find "$RECORDINGS_PATH/$today" -type f -name "*.$format" 2>/dev/null | wc -l) fi fi cat </dev/null | while read -r file; do local name=$(basename "$file") local size=$(du -h "$file" 2>/dev/null | cut -f1) local time=$(echo "$name" | cut -d'-' -f1) local caller=$(echo "$name" | cut -d'-' -f2) local dest=$(echo "$name" | cut -d'-' -f3 | sed "s/\.$format//") printf " %s %s -> %s (%s)\n" "$time" "$caller" "$dest" "$size" done else log_warn "No recordings found for $date_filter" fi else # List all recording dates with counts echo "Recording dates:" echo "---------------" find "$RECORDINGS_PATH" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort -r | while read -r dir; do local date=$(basename "$dir") local count=$(find "$dir" -type f -name "*.$format" 2>/dev/null | wc -l) local size=$(du -sh "$dir" 2>/dev/null | cut -f1) printf " %s: %d recordings (%s)\n" "$date" "$count" "$size" done fi } cmd_rec_play() { local file="$1" [ -z "$file" ] && { log_err "Usage: voipctl rec play " return 1 } # Find the file (could be full path or just filename) local fullpath if [ -f "$file" ]; then fullpath="$file" else fullpath=$(find "$RECORDINGS_PATH" -name "$file" -type f 2>/dev/null | head -1) fi if [ -z "$fullpath" ] || [ ! -f "$fullpath" ]; then log_err "Recording not found: $file" return 1 fi # Try to play the file if command -v aplay >/dev/null 2>&1; then log_info "Playing: $fullpath" aplay "$fullpath" elif command -v ffplay >/dev/null 2>&1; then ffplay -nodisp -autoexit "$fullpath" else echo "File: $fullpath" log_info "No audio player available. Download the file to play." fi } cmd_rec_download() { local file="$1" [ -z "$file" ] && { log_err "Usage: voipctl rec download " return 1 } local fullpath if [ -f "$file" ]; then fullpath="$file" else fullpath=$(find "$RECORDINGS_PATH" -name "$file" -type f 2>/dev/null | head -1) fi if [ -z "$fullpath" ] || [ ! -f "$fullpath" ]; then log_err "Recording not found: $file" return 1 fi echo "$fullpath" } cmd_rec_delete() { local file="$1" [ -z "$file" ] && { log_err "Usage: voipctl rec delete " return 1 } local fullpath if [ -f "$file" ]; then fullpath="$file" else fullpath=$(find "$RECORDINGS_PATH" -name "$file" -type f 2>/dev/null | head -1) fi if [ -z "$fullpath" ] || [ ! -f "$fullpath" ]; then log_err "Recording not found: $file" return 1 fi rm -f "$fullpath" log_ok "Deleted: $(basename "$fullpath")" # Clean up empty directories find "$RECORDINGS_PATH" -type d -empty -delete 2>/dev/null } cmd_rec_cleanup() { local days="${1:-30}" log_info "Cleaning up recordings older than $days days..." local count=0 local freed=0 if [ -d "$RECORDINGS_PATH" ]; then # Calculate size before local before=$(du -s "$RECORDINGS_PATH" 2>/dev/null | cut -f1 || echo 0) # Delete old recordings count=$(find "$RECORDINGS_PATH" -type f -mtime +$days 2>/dev/null | wc -l) find "$RECORDINGS_PATH" -type f -mtime +$days -delete 2>/dev/null # Remove empty directories find "$RECORDINGS_PATH" -type d -empty -delete 2>/dev/null # Calculate freed space local after=$(du -s "$RECORDINGS_PATH" 2>/dev/null | cut -f1 || echo 0) freed=$(( (before - after) / 1024 )) fi log_ok "Deleted $count recordings, freed ${freed}MB" } cmd_rec_list_json() { local date_filter="$1" local format=$(uci -q get voip.recording.format || echo "wav") echo "[" local first=1 if [ -n "$date_filter" ]; then local dir="$RECORDINGS_PATH/$date_filter" if [ -d "$dir" ]; then find "$dir" -type f -name "*.$format" 2>/dev/null | sort -r | while read -r file; do local name=$(basename "$file") local size=$(stat -c%s "$file" 2>/dev/null || echo 0) local mtime=$(stat -c%Y "$file" 2>/dev/null || echo 0) local time=$(echo "$name" | cut -d'-' -f1) local caller=$(echo "$name" | cut -d'-' -f2) local dest=$(echo "$name" | cut -d'-' -f3 | sed "s/\.$format//") [ $first -eq 0 ] && echo "," first=0 cat </dev/null | sort -r | head -50 | while read -r file; do local name=$(basename "$file") local dir=$(dirname "$file") local date=$(basename "$dir") local size=$(stat -c%s "$file" 2>/dev/null || echo 0) local mtime=$(stat -c%Y "$file" 2>/dev/null || echo 0) local time=$(echo "$name" | cut -d'-' -f1) local caller=$(echo "$name" | cut -d'-' -f2) local dest=$(echo "$name" | cut -d'-' -f3 | sed "s/\.$format//") [ $first -eq 0 ] && echo "," first=0 cat <> /etc/haproxy/conf.d/voip.cfg <" return 1 } log_info "Emancipating VoIP at $domain..." # Configure domain uci set voip.ssl.enabled="1" uci set voip.ssl.domain="$domain" uci commit voip # Configure HAProxy cmd_configure_haproxy # Request SSL certificate if [ -f /usr/sbin/acmectl ]; then acmectl issue "$domain" fi log_ok "VoIP exposed at https://$domain" } cmd_reload() { if ! container_running; then log_err "Container not running" return 1 fi log_info "Reloading Asterisk configuration..." generate_asterisk_config container_exec asterisk -rx "core reload" log_ok "Configuration reloaded" } # # Main # case "$1" in install) cmd_install ;; uninstall) cmd_uninstall ;; start) cmd_start ;; stop) cmd_stop ;; restart) cmd_restart ;; status) cmd_status ;; logs) shift cmd_logs "$@" ;; shell) cmd_shell ;; ext) case "$2" in add) shift 2; cmd_ext_add "$@" ;; del) shift 2; cmd_ext_del "$@" ;; list) cmd_ext_list ;; passwd) shift 2; cmd_ext_passwd "$@" ;; *) usage ;; esac ;; trunk) case "$2" in add) case "$3" in ovh) cmd_trunk_add_ovh ;; manual) log_info "Edit /etc/config/voip sip_trunk section" ;; *) usage ;; esac ;; test) cmd_trunk_test ;; status) cmd_trunk_status ;; *) usage ;; esac ;; call) shift cmd_call "$@" ;; hangup) shift cmd_hangup "$@" ;; calls) cmd_calls ;; vm) case "$2" in list) shift 2; cmd_vm_list "$@" ;; play) shift 2; cmd_vm_play "$@" ;; delete) shift 2; cmd_vm_delete "$@" ;; *) usage ;; esac ;; rec) case "$2" in enable) cmd_rec_enable ;; disable) cmd_rec_disable ;; status) cmd_rec_status ;; list) shift 2; cmd_rec_list "$@" ;; list-json) shift 2; cmd_rec_list_json "$@" ;; play) shift 2; cmd_rec_play "$@" ;; download) shift 2; cmd_rec_download "$@" ;; delete) shift 2; cmd_rec_delete "$@" ;; cleanup) shift 2; cmd_rec_cleanup "$@" ;; *) usage ;; esac ;; configure-haproxy) cmd_configure_haproxy ;; emancipate) shift cmd_emancipate "$@" ;; reload) cmd_reload ;; *) usage ;; esac