diff --git a/easy-asterisk-v0.9.9.sh b/easy-asterisk-v0.10.0.sh similarity index 78% rename from easy-asterisk-v0.9.9.sh rename to easy-asterisk-v0.10.0.sh index d5e142d..377f24b 100644 --- a/easy-asterisk-v0.9.9.sh +++ b/easy-asterisk-v0.10.0.sh @@ -1,12 +1,26 @@ #!/bin/bash # ================================================================ -# Easy Asterisk - Interactive Installer v0.9.9 +# Easy Asterisk - Interactive Installer v0.10.0 # # Copyright (C) 2025 Easy Asterisk Contributors # Licensed under GNU General Public License v3.0 # See LICENSE file or https://www.gnu.org/licenses/gpl-3.0.html # -# UPDATES in v0.9.9: +# UPDATES in v0.10.0: +# - FIXED: Extension deletion now properly removes all sections (endpoint, auth, aor) +# - FIXED: Extension renaming now preserves AA tags correctly +# - FIXED: LAN/VPN devices now explicitly use UDP transport (prevents TLS fallback) +# - FIXED: LAN devices now have media_encryption=no to prevent SRTP issues +# - ADDED: Web Admin interface for browser-based client management +# - View device status (online/offline) in real-time +# - Add/delete devices via web interface +# - View rooms and categories +# - HTTP Basic authentication with SHA256 password hashing +# - Access at http://server:8080/clients +# - IMPROVED: Device deletion uses awk for reliable multi-section removal +# - IMPROVED: Device renaming uses awk to handle all edge cases +# +# PREVIOUS UPDATES (v0.9.9): # - REMOVED: All COTURN/TURN relay server code (focus on direct connections) # - ADDED: VLAN subnet configuration to prevent 30-second call drops # - ADDED: Provisioning Manager (http.conf setup, symlinks, linphone.xml editor) @@ -15,11 +29,6 @@ # - ADDED: Split-horizon DNS documentation for VLAN environments # - IMPROVED: Server IP address documented in transport configurations # - IMPROVED: Multiple local_net entries for proper VLAN support -# -# PREVIOUS UPDATES (v0.9.8): -# - FIXED: Categories not displaying correctly in lists of devices -# - ADDED: Client export/import functionality in Device Management menu -# - ADDED: Ability to rename categories and rooms # ================================================================ set +e @@ -41,7 +50,7 @@ PTT_CONFIG_FILE="${CONFIG_DIR}/ptt-device" CATEGORIES_FILE="${CONFIG_DIR}/categories.conf" ROOMS_FILE="${CONFIG_DIR}/rooms.conf" PROVISIONING_DIR="/var/lib/asterisk/static-http" -SCRIPT_VERSION="0.9.9" +SCRIPT_VERSION="0.10.0" # ================================================================ # 1. CORE HELPER FUNCTIONS @@ -612,7 +621,9 @@ add_device_menu() { conn_choice="${conn_choice:-1}" if [[ "$conn_choice" == "1" ]]; then - # LAN/VPN - UDP, no encryption + # LAN/VPN - UDP, no encryption (explicit transport prevents TLS fallback) + transport_block="transport=transport-udp" + encryption_block="media_encryption=no" display_server="$(hostname -I | awk '{print $1}')" display_port="5060" display_transport="UDP" @@ -640,6 +651,7 @@ add_device_menu() { [${ext}] type=endpoint context=intercom +${transport_block} disallow=all allow=opus allow=ulaw @@ -756,24 +768,25 @@ EOF remove_device() { print_header "Remove Device" declare -A REMOVE_MAP + declare -A NAME_MAP local count=1 + local current_name="" echo "Select device to remove:" echo "" while IFS= read -r line; do if [[ "$line" == *"; === Device:"* ]]; then local temp="${line#*; === Device: }" temp="${temp% ===}" - temp="${temp% \[AA:*\]}" - local name="${temp% (*)}" + temp="${temp% \[AA:*\]}" + current_name="${temp% (*)}" fi - if [[ "$line" =~ ^\[([0-9]+)\] ]]; then + if [[ "$line" =~ ^\[([0-9]+)\]$ && "$current_name" != "" ]]; then local ext="${BASH_REMATCH[1]}" - if [[ -n "$name" ]]; then - echo " ${count}) Ext ${ext} - ${name}" - REMOVE_MAP[$count]=$ext - ((count++)) - name="" - fi + echo " ${count}) Ext ${ext} - ${current_name}" + REMOVE_MAP[$count]=$ext + NAME_MAP[$count]="$current_name" + ((count++)) + current_name="" fi done < /etc/asterisk/pjsip.conf echo "" @@ -781,16 +794,23 @@ remove_device() { 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 + # Remove all device sections - use awk to properly handle all sections + awk ' + /^; === Device:/ { skip = 1; next } + /^\[[0-9]{3}\]$/ { if (skip) next } + /^type=(endpoint|auth|aor)/ { if (skip) next } + /^$/ { if (skip) { skip = 0; next } } + !skip { print } + ' /etc/asterisk/pjsip.conf > /etc/asterisk/pjsip.conf.tmp + mv /etc/asterisk/pjsip.conf.tmp /etc/asterisk/pjsip.conf + chown asterisk:asterisk /etc/asterisk/pjsip.conf asterisk -rx "pjsip reload" 2>/dev/null rebuild_dialplan print_success "All devices deleted" @@ -799,18 +819,48 @@ remove_device() { fi return fi - + [[ "$choice" == "0" || -z "${REMOVE_MAP[$choice]}" ]] && return - + local ext="${REMOVE_MAP[$choice]}" - read -p "Confirm removal of $ext? [y/N]: " confirm + local name="${NAME_MAP[$choice]}" + read -p "Confirm removal of $ext ($name)? [y/N]: " confirm if [[ "$confirm" =~ ^[Yy]$ ]]; then backup_config "/etc/asterisk/pjsip.conf" - sed -i "/^; === Device:.*${ext}.*/,/^$/d" /etc/asterisk/pjsip.conf - sed -i "/^\[${ext}\]/,/^$/d" /etc/asterisk/pjsip.conf + # Use awk to remove the device comment and ALL three sections for this extension + awk -v ext="$ext" ' + BEGIN { skip = 0; found_ext = 0 } + # Match device comment line - start potential skip + /^; === Device:/ { pending_comment = $0; next } + # Check if this is the extension we want to delete + $0 ~ "^\\[" ext "\\]$" { + if (pending_comment != "") { + # This is our device - skip the comment and this section + skip = 1 + found_ext = 1 + pending_comment = "" + next + } else if (found_ext) { + # Additional sections for same extension (auth, aor) + skip = 1 + next + } + } + # If we have a pending comment for a different extension, print it + pending_comment != "" && $0 !~ "^\\[" ext "\\]$" { + print pending_comment + pending_comment = "" + } + # Skip lines until empty line + skip && /^$/ { skip = 0; next } + skip { next } + { print } + ' /etc/asterisk/pjsip.conf > /etc/asterisk/pjsip.conf.tmp + mv /etc/asterisk/pjsip.conf.tmp /etc/asterisk/pjsip.conf + chown asterisk:asterisk /etc/asterisk/pjsip.conf asterisk -rx "pjsip reload" 2>/dev/null rebuild_dialplan - print_success "Removed" + print_success "Removed extension $ext ($name)" fi } @@ -818,6 +868,7 @@ rename_device() { print_header "Rename Device" declare -A DEVICE_MAP declare -A NAME_MAP + declare -A AA_MAP local count=1 echo "Select device to rename:" echo "" @@ -825,55 +876,80 @@ rename_device() { if [[ "$line" == *"; === Device:"* ]]; then local temp="${line#*; === Device: }" temp="${temp% ===}" - temp="${temp% \[AA:*\]}" + local aa_tag="" + if [[ "$temp" == *"[AA:yes]"* ]]; then + aa_tag="[AA:yes]" + temp="${temp% \[AA:yes\]}" + elif [[ "$temp" == *"[AA:no]"* ]]; then + aa_tag="[AA:no]" + temp="${temp% \[AA:no\]}" + fi local name="${temp% (*)}" local cat="${temp##* (}"; cat="${cat%)}" fi - if [[ "$line" =~ ^\[([0-9]+)\] ]]; then + if [[ "$line" =~ ^\[([0-9]+)\]$ && -n "$name" ]]; 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 + echo " ${count}) Ext ${ext} - ${name} (${cat})" + DEVICE_MAP[$count]=$ext + NAME_MAP[$count]="${name}|${cat}" + AA_MAP[$count]="${aa_tag}" + ((count++)) + name="" fi done < /etc/asterisk/pjsip.conf echo "" echo " 0) Cancel" echo "" read -p "Select: " choice - + [[ "$choice" == "0" || -z "${DEVICE_MAP[$choice]}" ]] && return - + local ext="${DEVICE_MAP[$choice]}" local info="${NAME_MAP[$choice]}" + local aa_tag="${AA_MAP[$choice]}" local old_name="${info%|*}" local cat="${info##*|}" - + echo "" echo "Current name: ${old_name}" read -p "New name: " new_name - + if [[ -z "$new_name" ]]; then print_error "Name cannot be empty" return fi - + # Backup config backup_config "/etc/asterisk/pjsip.conf" - - # 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 - + + # Use awk to properly update both the comment line (preserving AA tag) and callerid + awk -v ext="$ext" -v old_name="$old_name" -v new_name="$new_name" -v cat="$cat" -v aa_tag="$aa_tag" ' + # Update device comment line + /^; === Device:/ && $0 ~ old_name && $0 ~ cat { + if (aa_tag != "") { + print "; === Device: " new_name " (" cat ") " aa_tag " ===" + } else { + print "; === Device: " new_name " (" cat ") ===" + } + next + } + # Track when we are in the correct extension section + $0 ~ "^\\[" ext "\\]$" { in_ext = 1 } + /^$/ { in_ext = 0 } + # Update callerid in the extension section + in_ext && /^callerid=/ { + print "callerid=\"" new_name "\" <" ext ">" + next + } + { print } + ' /etc/asterisk/pjsip.conf > /etc/asterisk/pjsip.conf.tmp + mv /etc/asterisk/pjsip.conf.tmp /etc/asterisk/pjsip.conf + chown asterisk:asterisk /etc/asterisk/pjsip.conf + # Reload Asterisk asterisk -rx "pjsip reload" 2>/dev/null rebuild_dialplan quiet - + print_success "Device renamed: ${old_name} → ${new_name}" } @@ -3613,6 +3689,1041 @@ submenu_install() { esac } +# ================================================================ +# WEB ADMIN INTERFACE +# ================================================================ + +WEB_ADMIN_PORT="8080" +WEB_ADMIN_SCRIPT="/usr/local/bin/easy-asterisk-webadmin" +WEB_ADMIN_SERVICE="/etc/systemd/system/easy-asterisk-webadmin.service" +WEB_ADMIN_HTPASSWD="/etc/easy-asterisk/webadmin.htpasswd" + +create_web_admin_script() { + cat > "$WEB_ADMIN_SCRIPT" << 'WEBADMIN' +#!/usr/bin/env python3 +""" +Easy Asterisk Web Admin - Simple web interface for client management +""" + +import http.server +import socketserver +import json +import subprocess +import os +import re +import base64 +import hashlib +import html +from urllib.parse import parse_qs, urlparse +from functools import partial + +PORT = int(os.environ.get('WEBADMIN_PORT', 8080)) +HTPASSWD_FILE = "/etc/easy-asterisk/webadmin.htpasswd" +PJSIP_CONF = "/etc/asterisk/pjsip.conf" +CATEGORIES_FILE = "/etc/easy-asterisk/categories.conf" +ROOMS_FILE = "/etc/easy-asterisk/rooms.conf" +CONFIG_FILE = "/etc/easy-asterisk/config" + +def check_auth(headers): + """Verify HTTP Basic Auth against htpasswd file""" + if not os.path.exists(HTPASSWD_FILE): + return True # No auth required if no htpasswd file + + auth_header = headers.get('Authorization', '') + if not auth_header.startswith('Basic '): + return False + + try: + credentials = base64.b64decode(auth_header[6:]).decode('utf-8') + username, password = credentials.split(':', 1) + + with open(HTPASSWD_FILE, 'r') as f: + for line in f: + line = line.strip() + if ':' in line: + stored_user, stored_hash = line.split(':', 1) + if stored_user == username: + # Support plain text (for simplicity) or SHA256 + if stored_hash.startswith('{SHA256}'): + expected = '{SHA256}' + hashlib.sha256(password.encode()).hexdigest() + return stored_hash == expected + else: + return stored_hash == password + return False + except: + return False + +def get_registered_endpoints(): + """Get list of registered endpoints from Asterisk""" + try: + result = subprocess.run( + ['asterisk', '-rx', 'pjsip show endpoints'], + capture_output=True, text=True, timeout=5 + ) + endpoints = {} + for line in result.stdout.split('\n'): + match = re.match(r'\s*(\d{3})/\S+\s+(\S+)\s+', line) + if match: + ext, state = match.groups() + endpoints[ext] = 'online' if 'Avail' in state else 'offline' + return endpoints + except: + return {} + +def get_devices(): + """Parse pjsip.conf to get device information""" + devices = [] + if not os.path.exists(PJSIP_CONF): + return devices + + with open(PJSIP_CONF, 'r') as f: + content = f.read() + + device_pattern = re.compile( + r'; === Device: ([^(]+)\(([^)]+)\)\s*(\[AA:(yes|no)\])?\s*===\s*\n' + r'\[(\d+)\]\s*\n' + r'type=endpoint\s*\n' + r'(?:.*?\n)*?' + r'(?:transport=transport-(\w+))?\s*\n?' + r'(?:.*?\n)*?' + r'(?:media_encryption=(\w+))?\s*\n?' + r'(?:.*?\n)*?' + r'\[\5\]\s*\n' + r'type=auth\s*\n' + r'(?:.*?\n)*?' + r'password=(\S+)', + re.MULTILINE + ) + + # Simpler parsing approach + current_device = None + in_endpoint = False + + for line in content.split('\n'): + line = line.strip() + + if line.startswith('; === Device:'): + match = re.match(r'; === Device: (.+?) \((\w+)\)\s*(\[AA:(yes|no)\])?\s*===', line) + if match: + current_device = { + 'name': match.group(1).strip(), + 'category': match.group(2), + 'auto_answer': match.group(4) if match.group(3) else None, + 'extension': None, + 'password': None, + 'transport': 'udp', + 'encryption': 'no' + } + + elif current_device and re.match(r'^\[(\d{3})\]$', line): + ext = re.match(r'^\[(\d{3})\]$', line).group(1) + if current_device['extension'] is None: + current_device['extension'] = ext + in_endpoint = True + + elif current_device and in_endpoint: + if line.startswith('transport=transport-'): + current_device['transport'] = line.split('-')[1] + elif line.startswith('media_encryption='): + current_device['encryption'] = line.split('=')[1] + elif line.startswith('password='): + current_device['password'] = line.split('=')[1] + devices.append(current_device) + current_device = None + in_endpoint = False + elif line == '' or line.startswith('['): + in_endpoint = False + + return devices + +def get_categories(): + """Get categories from config file""" + categories = [] + if os.path.exists(CATEGORIES_FILE): + with open(CATEGORIES_FILE, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + parts = line.split('|') + if len(parts) >= 3: + categories.append({ + 'id': parts[0], + 'name': parts[1], + 'auto_answer': parts[2], + 'description': parts[3] if len(parts) > 3 else '' + }) + return categories + +def get_rooms(): + """Get rooms from config file""" + rooms = [] + if os.path.exists(ROOMS_FILE): + with open(ROOMS_FILE, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + parts = line.split('|') + if len(parts) >= 5: + rooms.append({ + 'extension': parts[0], + 'name': parts[1], + 'members': parts[2], + 'timeout': parts[3], + 'type': parts[4] + }) + return rooms + +def delete_device(extension): + """Delete a device from pjsip.conf""" + if not os.path.exists(PJSIP_CONF): + return False, "Config file not found" + + with open(PJSIP_CONF, 'r') as f: + lines = f.readlines() + + new_lines = [] + skip = False + found = False + pending_comment = None + + for line in lines: + stripped = line.strip() + + if stripped.startswith('; === Device:'): + pending_comment = line + continue + + if re.match(rf'^\[{extension}\]$', stripped): + if pending_comment: + found = True + skip = True + pending_comment = None + continue + elif found: + skip = True + continue + + if pending_comment: + new_lines.append(pending_comment) + pending_comment = None + + if skip and stripped == '': + skip = False + continue + + if not skip: + new_lines.append(line) + + if found: + with open(PJSIP_CONF, 'w') as f: + f.writelines(new_lines) + subprocess.run(['asterisk', '-rx', 'pjsip reload'], capture_output=True) + return True, "Device deleted" + return False, "Device not found" + +def generate_password(length=16): + """Generate a random password""" + import secrets + import string + chars = string.ascii_letters + string.digits + return ''.join(secrets.choice(chars) for _ in range(length)) + +def add_device(name, category, extension, conn_type='lan', auto_answer=None): + """Add a new device to pjsip.conf""" + if not os.path.exists(PJSIP_CONF): + return False, "Config file not found" + + # Check if extension exists + with open(PJSIP_CONF, 'r') as f: + if f'[{extension}]' in f.read(): + return False, "Extension already exists" + + password = generate_password() + + # Determine transport and encryption + if conn_type == 'fqdn': + transport = 'transport=transport-tls' + encryption = 'media_encryption=sdes' + ice = 'ice_support=yes' + else: + transport = 'transport=transport-udp' + encryption = 'media_encryption=no' + ice = '' + + aa_tag = '' + if auto_answer == 'yes': + aa_tag = '[AA:yes] ' + elif auto_answer == 'no': + aa_tag = '[AA:no] ' + + device_config = f''' +; === Device: {name} ({category}) {aa_tag}=== +[{extension}] +type=endpoint +context=intercom +{transport} +disallow=all +allow=opus +allow=ulaw +allow=alaw +allow=g722 +{encryption} +direct_media=no +rtp_symmetric=yes +force_rport=yes +rewrite_contact=yes +{ice} +auth={extension} +aors={extension} +callerid="{name}" <{extension}> + +[{extension}] +type=auth +auth_type=userpass +username={extension} +password={password} + +[{extension}] +type=aor +max_contacts=5 +remove_existing=yes +qualify_frequency=60 +''' + + with open(PJSIP_CONF, 'a') as f: + f.write(device_config) + + subprocess.run(['asterisk', '-rx', 'pjsip reload'], capture_output=True) + subprocess.run(['chown', 'asterisk:asterisk', PJSIP_CONF], capture_output=True) + + return True, {'extension': extension, 'password': password, 'name': name} + +def get_server_info(): + """Get server configuration info""" + info = { + 'domain': '', + 'tls_enabled': False, + 'server_ip': '' + } + + if os.path.exists(CONFIG_FILE): + with open(CONFIG_FILE, 'r') as f: + for line in f: + if line.startswith('DOMAIN_NAME='): + info['domain'] = line.split('=', 1)[1].strip().strip('"') + elif line.startswith('ENABLE_TLS='): + info['tls_enabled'] = 'y' in line.lower() + + try: + result = subprocess.run(['hostname', '-I'], capture_output=True, text=True) + info['server_ip'] = result.stdout.split()[0] if result.stdout else '' + except: + pass + + return info + +HTML_TEMPLATE = ''' + + + + + Easy Asterisk - Client Admin + + + +
+
+

Easy Asterisk - Client Admin

+

Manage SIP clients and extensions

+
+ +
+ +
+
Devices
+
Rooms
+
Categories
+
+ +
+
+
+

Registered Devices

+
+ + +
+
+
+ + + + + + + + + + + + +
ExtensionNameCategoryTransportStatusActions
+
+
+
+ +
+
+
+

Rooms (Ring/Page Groups)

+ +
+
+ + + + + + + + + + + +
ExtensionNameTypeMembersTimeout
+
+
+
+ +
+
+
+

Device Categories

+ +
+
+ + + + + + + + + + +
IDNameAuto-AnswerDescription
+
+
+
+
+ + + + + + + + + + +''' + +class WebAdminHandler(http.server.BaseHTTPRequestHandler): + def log_message(self, format, *args): + pass # Suppress default logging + + def send_auth_required(self): + self.send_response(401) + self.send_header('WWW-Authenticate', 'Basic realm="Easy Asterisk Admin"') + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(b'

Authentication Required

') + + def do_GET(self): + if not check_auth(self.headers): + self.send_auth_required() + return + + path = urlparse(self.path).path + + if path == '/' or path == '/clients': + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(HTML_TEMPLATE.encode()) + + elif path == '/api/devices': + devices = get_devices() + self.send_json(devices) + + elif path == '/api/status': + status = get_registered_endpoints() + self.send_json(status) + + elif path == '/api/categories': + categories = get_categories() + self.send_json(categories) + + elif path == '/api/rooms': + rooms = get_rooms() + self.send_json(rooms) + + elif path == '/api/server': + info = get_server_info() + self.send_json(info) + + else: + self.send_response(404) + self.end_headers() + + def do_POST(self): + if not check_auth(self.headers): + self.send_auth_required() + return + + path = urlparse(self.path).path + content_length = int(self.headers.get('Content-Length', 0)) + body = self.rfile.read(content_length).decode('utf-8') + + if path == '/api/devices': + try: + data = json.loads(body) + success, result = add_device( + data['name'], + data['category'], + data['extension'], + data.get('conn_type', 'lan'), + data.get('auto_answer') + ) + if success: + self.send_json({'success': True, 'data': result}) + else: + self.send_json({'success': False, 'error': result}, 400) + except Exception as e: + self.send_json({'success': False, 'error': str(e)}, 400) + else: + self.send_response(404) + self.end_headers() + + def do_DELETE(self): + if not check_auth(self.headers): + self.send_auth_required() + return + + path = urlparse(self.path).path + match = re.match(r'/api/devices/(\d+)', path) + + if match: + ext = match.group(1) + success, msg = delete_device(ext) + self.send_json({'success': success, 'message': msg}) + else: + self.send_response(404) + self.end_headers() + + def send_json(self, data, status=200): + self.send_response(status) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps(data).encode()) + +def main(): + with socketserver.TCPServer(("", PORT), WebAdminHandler) as httpd: + print(f"Easy Asterisk Web Admin running on port {PORT}") + httpd.serve_forever() + +if __name__ == "__main__": + main() +WEBADMIN + chmod +x "$WEB_ADMIN_SCRIPT" + print_success "Web admin script created" +} + +create_web_admin_service() { + cat > "$WEB_ADMIN_SERVICE" << EOF +[Unit] +Description=Easy Asterisk Web Admin +After=network.target asterisk.service + +[Service] +Type=simple +Environment=WEBADMIN_PORT=${WEB_ADMIN_PORT} +ExecStart=/usr/bin/python3 ${WEB_ADMIN_SCRIPT} +Restart=always +RestartSec=5 +User=root + +[Install] +WantedBy=multi-user.target +EOF + systemctl daemon-reload + print_success "Web admin service created" +} + +setup_web_admin_auth() { + print_header "Web Admin Authentication" + echo "Set up login credentials for the web admin interface." + echo "" + + read -p "Username [admin]: " wa_user + wa_user="${wa_user:-admin}" + + while true; do + read -s -p "Password: " wa_pass + echo "" + if [[ ${#wa_pass} -lt 6 ]]; then + print_error "Password must be at least 6 characters" + continue + fi + read -s -p "Confirm password: " wa_pass2 + echo "" + if [[ "$wa_pass" != "$wa_pass2" ]]; then + print_error "Passwords don't match" + continue + fi + break + done + + # Store with SHA256 hash + local hash=$(echo -n "$wa_pass" | sha256sum | awk '{print $1}') + echo "${wa_user}:{SHA256}${hash}" > "$WEB_ADMIN_HTPASSWD" + chmod 600 "$WEB_ADMIN_HTPASSWD" + print_success "Authentication configured for user: $wa_user" +} + +web_admin_menu() { + load_config + local server_ip=$(hostname -I | awk '{print $1}') + + print_header "Web Admin Management" + + # Check current status + local status="stopped" + if systemctl is-active --quiet easy-asterisk-webadmin 2>/dev/null; then + status="running" + fi + + echo " Status: ${status^^}" + if [[ "$status" == "running" ]]; then + echo " URL: http://${server_ip}:${WEB_ADMIN_PORT}/clients" + [[ -n "$DOMAIN_NAME" ]] && echo " URL: http://${DOMAIN_NAME}:${WEB_ADMIN_PORT}/clients" + fi + echo "" + echo " 1) Start Web Admin" + echo " 2) Stop Web Admin" + echo " 3) Restart Web Admin" + echo " 4) Configure Authentication" + echo " 5) Change Port (current: ${WEB_ADMIN_PORT})" + echo " 6) View Logs" + echo " 0) Back" + echo "" + read -p " Select: " choice + + case $choice in + 1) + if [[ ! -f "$WEB_ADMIN_SCRIPT" ]]; then + print_info "Installing web admin..." + create_web_admin_script + create_web_admin_service + fi + if [[ ! -f "$WEB_ADMIN_HTPASSWD" ]]; then + setup_web_admin_auth + fi + systemctl enable easy-asterisk-webadmin + systemctl start easy-asterisk-webadmin + sleep 2 + if systemctl is-active --quiet easy-asterisk-webadmin; then + print_success "Web Admin started" + echo "" + echo " Access at: http://${server_ip}:${WEB_ADMIN_PORT}/clients" + [[ -n "$DOMAIN_NAME" ]] && echo " Or: http://${DOMAIN_NAME}:${WEB_ADMIN_PORT}/clients" + else + print_error "Failed to start. Check: journalctl -u easy-asterisk-webadmin" + fi + ;; + 2) + systemctl stop easy-asterisk-webadmin + systemctl disable easy-asterisk-webadmin 2>/dev/null + print_success "Web Admin stopped" + ;; + 3) + systemctl restart easy-asterisk-webadmin + print_success "Web Admin restarted" + ;; + 4) + setup_web_admin_auth + systemctl restart easy-asterisk-webadmin 2>/dev/null + ;; + 5) + read -p "New port [${WEB_ADMIN_PORT}]: " new_port + new_port="${new_port:-$WEB_ADMIN_PORT}" + if [[ "$new_port" =~ ^[0-9]+$ ]] && [[ "$new_port" -ge 1024 ]] && [[ "$new_port" -le 65535 ]]; then + WEB_ADMIN_PORT="$new_port" + create_web_admin_service + systemctl restart easy-asterisk-webadmin 2>/dev/null + print_success "Port changed to $new_port" + else + print_error "Invalid port (must be 1024-65535)" + fi + ;; + 6) + journalctl -u easy-asterisk-webadmin -n 50 --no-pager + ;; + 0) return ;; + esac +} + submenu_server() { clear print_header "Server Settings" @@ -3626,6 +4737,7 @@ submenu_server() { echo " 8) Router Doctor" echo " 9) Configure VLAN Subnets" echo " 10) Provisioning Manager" + echo " 11) Web Admin (Client Management)" echo " 0) Back" read -p " Select: " choice case $choice in @@ -3639,6 +4751,7 @@ submenu_server() { 8) router_doctor ;; 9) configure_vlan_subnets ;; 10) provisioning_manager_menu ;; + 11) web_admin_menu ;; 0) return ;; esac [[ "$choice" != "0" ]] && read -p "Press Enter..."