Compare commits

..
8 Commits
12 changed files with 198 additions and 248 deletions

No files matched your search

+12 -8
View File
@@ -47,30 +47,34 @@
```text ```text
nexus-pbx/ nexus-pbx/
├── install.sh # Script d'installation automatisé tout-en-un (Debian 12 / 13)
├── public/ # Racine Web Nginx ├── public/ # Racine Web Nginx
│ ├── index.php # Interface Web SPA moderne │ ├── index.php # Interface Web SPA moderne
│ ├── api.php # API RESTful backend │ ├── api.php # API RESTful backend
│ ├── phonebook.php # Générateur XML Yealink Remote Phonebook │ ├── phonebook.php # Générateur XML Yealink Remote Phonebook
│ ├── assets/ # JavaScript (app.js), styles Tailwind CSS │ ├── assets/ # Logo, Favicon, JS (app.js), styles Tailwind CSS
│ └── avatars/ # Photos des contacts de l'annuaire │ └── avatars/ # Photos des contacts de l'annuaire
├── src/ # Classes PHP d'orchestration ├── src/ # Classes PHP d'orchestration
│ ├── AmiClient.php # Client Asterisk Manager Interface (AMI) │ ├── AmiClient.php # Client Asterisk Manager Interface (AMI)
│ ├── Auth.php # Authentification & sessions │ ├── Auth.php # Authentification & sessions
│ ├── BackupManager.php # Moteur de sauvegarde locale & distante SSH │ ├── BackupManager.php # Moteur Disaster Recovery & sauvegardes
│ ├── CertificateManager.php # Gestionnaire SSL & Certbot │ ├── CdrManager.php # Moteur Journal d'appels & CDR
│ ├── CertificateManager.php # Gestionnaire SSL & Certbot ACME
│ ├── ConfigGenerator.php # Générateur dynamique de conf Asterisk (pjsip, extensions) │ ├── ConfigGenerator.php # Générateur dynamique de conf Asterisk (pjsip, extensions)
│ ├── Database.php # Connexion PDO SQLite │ ├── Database.php # Connexion PDO SQLite
│ ├── FirewallManager.php # Moteur Pare-feu & règles Nginx │ ├── FirewallManager.php # Moteur Pare-feu Fail2ban & Nginx
│ ├── PhonebookManager.php # Moteur d'annuaire et export XML │ ├── PhonebookManager.php # Moteur d'annuaire et export XML
│ ├── SystemChecker.php # Diagnostic de santé système │ ├── SystemChecker.php # Diagnostic de santé système
│ └── SystemMetrics.php # Monitoring CPU, RAM, Disque, Asterisk │ └── SystemMetrics.php # Monitoring CPU, RAM, Disque, Asterisk
├── scripts/ # Scripts CLI et daemons système ├── scripts/ # Scripts CLI et daemons système
│ ├── setup-debian.sh # Script d'installation automatisé Debian 12 / 13 │ ├── update.sh # Script de mise à jour OTA (nexus-update)
│ ├── nexus-firewall # Script de gestion iptables/nftables │ ├── nexus-firewall # Outil CLI Pare-feu & Fail2ban
│ └── nexus_autobackup.php# Cronjob de sauvegarde automatique │ ├── nexus-banner.sh # Bannière d'accueil interactive SSH/Console
│ ├── nexus_autobackup.php# Cronjob de sauvegarde automatique
│ └── voicemail_pass_sync.php # Synchronisation code PIN messagerie vocale
└── storage/ # Données persistantes (exclues du Git) └── storage/ # Données persistantes (exclues du Git)
├── database.sqlite # Base de données applicative ├── database.sqlite # Base de données applicative
├── backups/ # Archives de sauvegarde ├── system_backups/ # Archives de sauvegarde Disaster Recovery
└── sounds/ # Fichiers audio MoH et annonces └── sounds/ # Fichiers audio MoH et annonces
``` ```
+6 -4
View File
@@ -1155,6 +1155,7 @@ try {
$send_rpid = (int)!empty($payload['send_rpid']); $send_rpid = (int)!empty($payload['send_rpid']);
$send_diversion = isset($payload['send_diversion']) ? (int)!empty($payload['send_diversion']) : 1; $send_diversion = isset($payload['send_diversion']) ? (int)!empty($payload['send_diversion']) : 1;
$trust_id_inbound = isset($payload['trust_id_inbound']) ? (int)!empty($payload['trust_id_inbound']) : 1; $trust_id_inbound = isset($payload['trust_id_inbound']) ? (int)!empty($payload['trust_id_inbound']) : 1;
$trust_id_outbound = isset($payload['trust_id_outbound']) ? (int)!empty($payload['trust_id_outbound']) : 1;
$max_retries = max(0, (int)($payload['max_retries'] ?? 10000)); $max_retries = max(0, (int)($payload['max_retries'] ?? 10000));
$forbidden_retry_interval = max(0, (int)($payload['forbidden_retry_interval'] ?? 600)); $forbidden_retry_interval = max(0, (int)($payload['forbidden_retry_interval'] ?? 600));
$fatal_retry_interval = max(0, (int)($payload['fatal_retry_interval'] ?? 0)); $fatal_retry_interval = max(0, (int)($payload['fatal_retry_interval'] ?? 0));
@@ -1228,6 +1229,7 @@ try {
':send_rpid' => $send_rpid, ':send_rpid' => $send_rpid,
':send_diversion' => $send_diversion, ':send_diversion' => $send_diversion,
':trust_id_inbound' => $trust_id_inbound, ':trust_id_inbound' => $trust_id_inbound,
':trust_id_outbound' => $trust_id_outbound,
':codecs' => $codecs, ':codecs' => $codecs,
':dtmf_mode' => $dtmf_mode, ':dtmf_mode' => $dtmf_mode,
':media_encryption' => $media_encryption, ':media_encryption' => $media_encryption,
@@ -1269,7 +1271,7 @@ try {
fatal_retry_interval = :fatal_retry_interval, line_support = :line_support, fatal_retry_interval = :fatal_retry_interval, line_support = :line_support,
from_user = :from_user, from_domain = :from_domain, outbound_callerid = :outbound_callerid, from_user = :from_user, from_domain = :from_domain, outbound_callerid = :outbound_callerid,
send_pai = :send_pai, send_rpid = :send_rpid, send_diversion = :send_diversion, send_pai = :send_pai, send_rpid = :send_rpid, send_diversion = :send_diversion,
trust_id_inbound = :trust_id_inbound, codecs = :codecs, dtmf_mode = :dtmf_mode, trust_id_inbound = :trust_id_inbound, trust_id_outbound = :trust_id_outbound, codecs = :codecs, dtmf_mode = :dtmf_mode,
media_encryption = :media_encryption, nat_symmetric = :nat_symmetric, media_encryption = :media_encryption, nat_symmetric = :nat_symmetric,
direct_media = :direct_media, qualify_frequency = :qualify_frequency, direct_media = :direct_media, qualify_frequency = :qualify_frequency,
max_channels = :max_channels, timers = :timers, language = :language, max_channels = :max_channels, timers = :timers, language = :language,
@@ -1291,7 +1293,7 @@ try {
registration_mode, register, server_uri, client_uri, contact_user, expiration, retry_interval, registration_mode, register, server_uri, client_uri, contact_user, expiration, retry_interval,
max_retries, forbidden_retry_interval, fatal_retry_interval, line_support, max_retries, forbidden_retry_interval, fatal_retry_interval, line_support,
from_user, from_domain, outbound_callerid, send_pai, send_rpid, send_diversion, from_user, from_domain, outbound_callerid, send_pai, send_rpid, send_diversion,
trust_id_inbound, codecs, dtmf_mode, media_encryption, nat_symmetric, direct_media, trust_id_inbound, trust_id_outbound, codecs, dtmf_mode, media_encryption, nat_symmetric, direct_media,
qualify_frequency, max_channels, timers, language, rtp_keepalive, t38_udptl, qualify_frequency, max_channels, timers, language, rtp_keepalive, t38_udptl,
fax_detect, inband_progress, match_ips, context, inbound_dest, did_routes_json, fax_detect, inband_progress, match_ips, context, inbound_dest, did_routes_json,
outbound_prefix, custom_endpoint_params, custom_reg_params, custom_aor_params, moh_sound_id, outbound_prefix, custom_endpoint_params, custom_reg_params, custom_aor_params, moh_sound_id,
@@ -1301,7 +1303,7 @@ try {
:registration_mode, :register, :server_uri, :client_uri, :contact_user, :expiration, :retry_interval, :registration_mode, :register, :server_uri, :client_uri, :contact_user, :expiration, :retry_interval,
:max_retries, :forbidden_retry_interval, :fatal_retry_interval, :line_support, :max_retries, :forbidden_retry_interval, :fatal_retry_interval, :line_support,
:from_user, :from_domain, :outbound_callerid, :send_pai, :send_rpid, :send_diversion, :from_user, :from_domain, :outbound_callerid, :send_pai, :send_rpid, :send_diversion,
:trust_id_inbound, :codecs, :dtmf_mode, :media_encryption, :nat_symmetric, :direct_media, :trust_id_inbound, :trust_id_outbound, :codecs, :dtmf_mode, :media_encryption, :nat_symmetric, :direct_media,
:qualify_frequency, :max_channels, :timers, :language, :rtp_keepalive, :t38_udptl, :qualify_frequency, :max_channels, :timers, :language, :rtp_keepalive, :t38_udptl,
:fax_detect, :inband_progress, :match_ips, :context, :inbound_dest, :did_routes_json, :fax_detect, :inband_progress, :match_ips, :context, :inbound_dest, :did_routes_json,
:outbound_prefix, :custom_endpoint_params, :custom_reg_params, :custom_aor_params, :moh_sound_id, :outbound_prefix, :custom_endpoint_params, :custom_reg_params, :custom_aor_params, :moh_sound_id,
@@ -1318,7 +1320,7 @@ try {
fatal_retry_interval = excluded.fatal_retry_interval, line_support = excluded.line_support, fatal_retry_interval = excluded.fatal_retry_interval, line_support = excluded.line_support,
from_user = excluded.from_user, from_domain = excluded.from_domain, outbound_callerid = excluded.outbound_callerid, from_user = excluded.from_user, from_domain = excluded.from_domain, outbound_callerid = excluded.outbound_callerid,
send_pai = excluded.send_pai, send_rpid = excluded.send_rpid, send_diversion = excluded.send_diversion, send_pai = excluded.send_pai, send_rpid = excluded.send_rpid, send_diversion = excluded.send_diversion,
trust_id_inbound = excluded.trust_id_inbound, codecs = excluded.codecs, dtmf_mode = excluded.dtmf_mode, trust_id_inbound = excluded.trust_id_inbound, trust_id_outbound = excluded.trust_id_outbound, codecs = excluded.codecs, dtmf_mode = excluded.dtmf_mode,
media_encryption = excluded.media_encryption, nat_symmetric = excluded.nat_symmetric, media_encryption = excluded.media_encryption, nat_symmetric = excluded.nat_symmetric,
direct_media = excluded.direct_media, qualify_frequency = excluded.qualify_frequency, direct_media = excluded.direct_media, qualify_frequency = excluded.qualify_frequency,
max_channels = excluded.max_channels, timers = excluded.timers, language = excluded.language, max_channels = excluded.max_channels, timers = excluded.timers, language = excluded.language,
+11 -2
View File
@@ -2355,6 +2355,7 @@ function openTrunkModal(trunk = null) {
form.elements['send_rpid'].checked = trunk ? Number(trunk.send_rpid ?? 0) === 1 : false; form.elements['send_rpid'].checked = trunk ? Number(trunk.send_rpid ?? 0) === 1 : false;
form.elements['send_diversion'].checked = trunk ? Number(trunk.send_diversion ?? 1) === 1 : true; form.elements['send_diversion'].checked = trunk ? Number(trunk.send_diversion ?? 1) === 1 : true;
form.elements['trust_id_inbound'].checked = trunk ? Number(trunk.trust_id_inbound ?? 1) === 1 : true; form.elements['trust_id_inbound'].checked = trunk ? Number(trunk.trust_id_inbound ?? 1) === 1 : true;
form.elements['trust_id_outbound'].checked = trunk ? Number(trunk.trust_id_outbound ?? 1) === 1 : true;
// Onglet 3 : Codecs, NAT, Timers, T.38 & Identify // Onglet 3 : Codecs, NAT, Timers, T.38 & Identify
form.elements['codecs'].value = trunk ? (trunk.codecs || 'alaw,ulaw,g722') : 'alaw,ulaw,g722'; form.elements['codecs'].value = trunk ? (trunk.codecs || 'alaw,ulaw,g722') : 'alaw,ulaw,g722';
@@ -3788,7 +3789,8 @@ document.addEventListener('DOMContentLoaded', () => {
payload.send_pai = form.elements['send_pai'].checked ? 1 : 0; payload.send_pai = form.elements['send_pai'].checked ? 1 : 0;
payload.send_rpid = form.elements['send_rpid'].checked ? 1 : 0; payload.send_rpid = form.elements['send_rpid'].checked ? 1 : 0;
payload.send_diversion = form.elements['send_diversion'].checked ? 1 : 0; payload.send_diversion = form.elements['send_diversion'].checked ? 1 : 0;
payload.trust_id_inbound = form.elements['trust_id_inbound'].checked ? 1 : 0; payload.trust_id_inbound = form.elements['trust_id_inbound'].checked ? 1 : 0;
payload.trust_id_outbound = form.elements['trust_id_outbound'].checked ? 1 : 0;
payload.nat_symmetric = form.elements['nat_symmetric'].checked ? 1 : 0; payload.nat_symmetric = form.elements['nat_symmetric'].checked ? 1 : 0;
payload.direct_media = form.elements['direct_media'].checked ? 1 : 0; payload.direct_media = form.elements['direct_media'].checked ? 1 : 0;
payload.inband_progress = form.elements['inband_progress'].checked ? 1 : 0; payload.inband_progress = form.elements['inband_progress'].checked ? 1 : 0;
@@ -4922,7 +4924,13 @@ function renderBackups() {
? `<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/30">Local</span>` ? `<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/30">Local</span>`
: `<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-purple-500/20 text-purple-300 border border-purple-500/30">Distant (SSH)</span>`; : `<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-purple-500/20 text-purple-300 border border-purple-500/30">Distant (SSH)</span>`;
const versionBadge = `<span class="px-1.5 py-0.2 rounded text-[10px] font-mono font-bold bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">v${escapeHtml(b.version || '1.2.0')}</span>`; const versionBadge = `<span class="px-1.5 py-0.2 rounded text-[10px] font-mono font-bold bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">v${escapeHtml(b.version || '1.6.2')}</span>`;
let srcBadge = '';
if (b.source === 'pre-update') {
srcBadge = `<span class="px-1.5 py-0.2 rounded text-[10px] font-bold bg-amber-500/20 text-amber-300 border border-amber-500/30" title="Sauvegarde automatique pré-mise à jour">Pré-MàJ</span>`;
} else if (b.source === 'scheduled') {
srcBadge = `<span class="px-1.5 py-0.2 rounded text-[10px] font-bold bg-cyan-500/20 text-cyan-300 border border-cyan-500/30" title="Sauvegarde planifiée automatique">Auto</span>`;
}
return ` return `
<tr class="hover:bg-anthracite-750 transition"> <tr class="hover:bg-anthracite-750 transition">
@@ -4933,6 +4941,7 @@ function renderBackups() {
</svg> </svg>
<span class="truncate max-w-xs sm:max-w-md">${escapeHtml(b.filename)}</span> <span class="truncate max-w-xs sm:max-w-md">${escapeHtml(b.filename)}</span>
${versionBadge} ${versionBadge}
${srcBadge}
</div> </div>
</td> </td>
<td class="px-4 py-3 text-slate-300 whitespace-nowrap">${escapeHtml(b.date_formatted)}</td> <td class="px-4 py-3 text-slate-300 whitespace-nowrap">${escapeHtml(b.date_formatted)}</td>
+4
View File
@@ -3949,6 +3949,10 @@ $appVersion = $appVerData['version'] ?? '1.5.0';
<input type="checkbox" name="trust_id_inbound" value="1" checked class="rounded bg-anthracite-900 border-anthracite-600"> <input type="checkbox" name="trust_id_inbound" value="1" checked class="rounded bg-anthracite-900 border-anthracite-600">
<span>Accepter l'identité entrante PAI/RPID (<code class="text-indigo-300">trust_id_inbound = yes</code>)</span> <span>Accepter l'identité entrante PAI/RPID (<code class="text-indigo-300">trust_id_inbound = yes</code>)</span>
</label> </label>
<label class="flex items-center gap-2 text-xs text-slate-200 cursor-pointer">
<input type="checkbox" name="trust_id_outbound" value="1" checked class="rounded bg-anthracite-900 border-anthracite-600">
<span>Transmettre l'identité sortante Caller ID (<code class="text-indigo-300">trust_id_outbound = yes</code>)</span>
</label>
</div> </div>
</div> </div>
</div> </div>
-198
View File
@@ -1,198 +0,0 @@
#!/usr/bin/env bash
# ==============================================================================
# NexusPBX - Script de Réparation Automatique des Permissions & Services
# ==============================================================================
set -e
if [ "$(id -u)" -ne 0 ]; then
echo "[ERREUR] Ce script doit être exécuté en root."
exit 1
fi
echo "-> Correction des répertoires Git et mise à jour..."
git config --global --add safe.directory /var/www/nexus-pbx 2>/dev/null || true
cd /var/www/nexus-pbx
git fetch origin main
git reset --hard origin/main
echo "-> Installation des dépendances système indispensables (ACL, ffmpeg, rsync, sox, sshpass)..."
which ffmpeg >/dev/null 2>&1 && which rsync >/dev/null 2>&1 && which setfacl >/dev/null 2>&1 && which sshpass >/dev/null 2>&1 || (apt-get update -y && apt-get install -y --no-install-recommends acl ffmpeg rsync sox sshpass)
echo "-> Création des dossiers et des fichiers de journalisation..."
mkdir -p /etc/fail2ban/filter.d /etc/asterisk /var/log/asterisk /var/log/asterisk/cdr-csv /var/log/nexus-pbx /var/www/nexus-pbx/storage/backups
touch /var/log/asterisk/messages /var/log/asterisk/security.log /var/log/asterisk/cdr-csv/Master.csv /var/log/nexus_security.log
chown asterisk:asterisk /var/log/asterisk/messages /var/log/asterisk/security.log /var/log/asterisk/cdr-csv/Master.csv 2>/dev/null || true
chmod 0664 /var/log/asterisk/messages /var/log/asterisk/security.log /var/log/asterisk/cdr-csv/Master.csv 2>/dev/null || true
chown www-data:asterisk /var/log/nexus_security.log 2>/dev/null || true
chmod 0664 /var/log/nexus_security.log 2>/dev/null || true
echo "-> Filtres Fail2ban Nexus-Web..."
cat << 'EOF' > /etc/fail2ban/filter.d/nexus-web.conf
[Definition]
failregex = ^.*\[NEXUS-WEB-AUTH-FAIL\]:\s*<HOST>.*$
ignoreregex =
EOF
chmod 0644 /etc/fail2ban/filter.d/nexus-web.conf
echo "-> Génération et application de /etc/fail2ban/jail.local..."
php -r '
require_once "/var/www/nexus-pbx/src/Database.php";
require_once "/var/www/nexus-pbx/src/FirewallManager.php";
$st = App\FirewallManager::getSettings();
App\FirewallManager::generateJailLocal(
$st["fail2ban_whitelist_ips"],
(int)$st["fail2ban_sip_maxretry"],
(int)$st["fail2ban_sip_findtime"],
(int)$st["fail2ban_sip_bantime"],
(int)$st["fail2ban_web_maxretry"],
(int)$st["fail2ban_web_bantime"],
(int)$st["admin_https_port"]
);
App\FirewallManager::applyNginxConfig((int)$st["admin_https_port"], (int)$st["certbot_strict_port80"]);
' 2>/dev/null || true
echo "-> Droits applicatifs et ACL POSIX (/etc/asterisk et /var/www/nexus-pbx)..."
usermod -aG asterisk www-data 2>/dev/null || true
usermod -aG www-data asterisk 2>/dev/null || true
mkdir -p /var/www/nexus-pbx/public/avatars /var/www/nexus-pbx/storage/sounds /var/www/nexus-pbx/storage/backups /var/www/nexus-pbx/storage/system_backups /etc/letsencrypt /var/log/letsencrypt /var/lib/letsencrypt
chgrp -R asterisk /etc/letsencrypt 2>/dev/null || true
chmod -R g+rX /etc/letsencrypt 2>/dev/null || true
chown -R www-data:asterisk /var/www/nexus-pbx
chmod 2775 /var/www/nexus-pbx/storage /var/www/nexus-pbx/storage/backups /var/www/nexus-pbx/storage/system_backups /var/www/nexus-pbx/storage/sounds /var/www/nexus-pbx/public/avatars 2>/dev/null || true
touch /etc/asterisk/pjsip.conf \
/etc/asterisk/pjsip.custom.conf \
/etc/asterisk/extensions.conf \
/etc/asterisk/extensions.custom.conf \
/etc/asterisk/musiconhold.conf \
/etc/asterisk/musiconhold.custom.conf \
/etc/asterisk/voicemail.conf \
/etc/asterisk/voicemail.custom.conf \
/etc/asterisk/modules.conf \
/etc/asterisk/rtp.conf \
/etc/asterisk/features.conf 2>/dev/null || true
# Désactivation des modules ODBC/IMAP incompatibles sur Debian (évite conflit bloquant app_voicemail)
if ! grep -q 'noload => app_voicemail_odbc.so' /etc/asterisk/modules.conf 2>/dev/null; then
if grep -q '\[modules\]' /etc/asterisk/modules.conf 2>/dev/null; then
sed -i '/\[modules\]/a noload => app_voicemail_odbc.so\nnoload => app_voicemail_imap.so' /etc/asterisk/modules.conf 2>/dev/null || true
else
printf '[modules]\nautoload=yes\nnoload => app_voicemail_odbc.so\nnoload => app_voicemail_imap.so\n' >> /etc/asterisk/modules.conf
fi
fi
# Inclusions Asterisk et configuration messagerie vocale
if ! grep -q 'pjsip.custom.conf' /etc/asterisk/pjsip.conf 2>/dev/null; then
printf '\n#tryinclude "pjsip.custom.conf"\n' >> /etc/asterisk/pjsip.conf
fi
if ! grep -q 'extensions.custom.conf' /etc/asterisk/extensions.conf 2>/dev/null; then
printf '\n#tryinclude "extensions.custom.conf"\n' >> /etc/asterisk/extensions.conf
fi
if ! grep -q 'musiconhold.custom.conf' /etc/asterisk/musiconhold.conf 2>/dev/null; then
sed -i 's/^\[default\]/;[default]/' /etc/asterisk/musiconhold.conf 2>/dev/null || true
sed -i 's/^mode=files/;mode=files/' /etc/asterisk/musiconhold.conf 2>/dev/null || true
sed -i 's/^directory=moh/;directory=moh/' /etc/asterisk/musiconhold.conf 2>/dev/null || true
printf '\n#tryinclude "/etc/asterisk/musiconhold.custom.conf"\n' >> /etc/asterisk/musiconhold.conf
fi
if ! grep -q 'voicemail.custom.conf' /etc/asterisk/voicemail.conf 2>/dev/null; then
sed -i 's/^1234 => 4242/;1234 => 4242/' /etc/asterisk/voicemail.conf 2>/dev/null || true
sed -i 's/^;\s*passwordlocation\s*=\s*spooldir/passwordlocation=spooldir/' /etc/asterisk/voicemail.conf 2>/dev/null || true
sed -i 's|^;\s*externpassnotify\s*=.*|externpassnotify=/usr/bin/php /var/www/nexus-pbx/scripts/voicemail_pass_sync.php|' /etc/asterisk/voicemail.conf 2>/dev/null || true
printf '\n#tryinclude "voicemail.custom.conf"\n' >> /etc/asterisk/voicemail.conf
fi
mkdir -p /var/lib/asterisk/sounds/custom /var/lib/asterisk/moh /var/spool/asterisk/voicemail/default 2>/dev/null || true
for lang in fr en es; do
if [ -d "/usr/share/asterisk/sounds/$lang" ] && [ ! -e "/var/lib/asterisk/sounds/$lang" ]; then
ln -sf "/usr/share/asterisk/sounds/$lang" "/var/lib/asterisk/sounds/$lang" 2>/dev/null || true
fi
if [ -d "/var/lib/asterisk/sounds/$lang" ] && [ ! -e "/var/lib/asterisk/sounds/$lang/custom" ]; then
ln -sf /var/lib/asterisk/sounds/custom "/var/lib/asterisk/sounds/$lang/custom" 2>/dev/null || true
fi
done
if [ ! -e "/usr/share/asterisk/sounds/custom" ]; then
ln -sf /var/lib/asterisk/sounds/custom /usr/share/asterisk/sounds/custom 2>/dev/null || true
fi
chown -R asterisk:asterisk /etc/asterisk /var/lib/asterisk /var/spool/asterisk /var/log/asterisk 2>/dev/null || true
chmod -R 775 /etc/asterisk /var/lib/asterisk/sounds /var/lib/asterisk/moh /var/spool/asterisk /var/log/asterisk 2>/dev/null || true
chmod 2775 /etc/asterisk /var/lib/asterisk/sounds /var/lib/asterisk/sounds/custom /var/lib/asterisk/moh /var/spool/asterisk /var/spool/asterisk/voicemail /var/log/asterisk /var/log/asterisk/cdr-csv 2>/dev/null || true
chmod 2750 /etc/asterisk/keys 2>/dev/null || true
chmod 0664 /etc/asterisk/*.conf /etc/asterisk/*.custom.conf 2>/dev/null || true
setfacl -R -m u:www-data:rwx,g:asterisk:rwx /etc/asterisk /var/lib/asterisk/sounds /var/lib/asterisk/moh /var/spool/asterisk /var/log/asterisk 2>/dev/null || true
setfacl -R -d -m u:www-data:rwx,g:asterisk:rwx /etc/asterisk /var/lib/asterisk/sounds /var/lib/asterisk/moh /var/spool/asterisk /var/log/asterisk 2>/dev/null || true
chown root:asterisk /etc/fail2ban/jail.local /etc/nginx/sites-available/nexus-pbx 2>/dev/null || true
chmod 0664 /etc/fail2ban/jail.local /etc/nginx/sites-available/nexus-pbx 2>/dev/null || true
setfacl -m u:www-data:rw /etc/fail2ban/jail.local /etc/nginx/sites-available/nexus-pbx 2>/dev/null || true
cat << 'EOF' > /etc/sudoers.d/nexus-pbx
www-data ALL=(ALL) NOPASSWD: /usr/sbin/asterisk, /usr/sbin/nginx, /usr/bin/systemctl, /usr/bin/fail2ban-client, /bin/fail2ban-client, /var/www/nexus-pbx/scripts/nexus-firewall, /usr/bin/certbot, /usr/sbin/nft, /usr/sbin/iptables, /var/www/nexus-pbx/scripts/update.sh, /usr/local/bin/nexus-update
EOF
chmod 0440 /etc/sudoers.d/nexus-pbx
echo "-> Autorisation d'écriture systemd pour PHP-FPM (Debian 12/13 ProtectSystem)..."
for fpm_svc in /lib/systemd/system/php*-fpm.service /usr/lib/systemd/system/php*-fpm.service; do
if [ -f "$fpm_svc" ]; then
svc_name=$(basename "$fpm_svc")
mkdir -p "/etc/systemd/system/${svc_name}.d"
cat << 'EOF_OVERRIDE' > "/etc/systemd/system/${svc_name}.d/override.conf"
[Service]
ProtectSystem=false
ReadWritePaths=/etc/asterisk /etc/fail2ban /etc/nginx /var/log /var/lib/asterisk /var/spool/asterisk /var/www /usr/local/bin /etc/sudoers.d
EOF_OVERRIDE
fi
done
systemctl daemon-reload
echo "-> Configuration des limites d'upload PHP (512 Mo)..."
for php_dir in /etc/php/*; do
if [ -d "$php_dir/fpm/conf.d" ]; then
cat << 'EOF_PHP' > "$php_dir/fpm/conf.d/99-nexus.ini"
upload_max_filesize = 512M
post_max_size = 512M
memory_limit = 512M
max_execution_time = 300
max_input_time = 300
EOF_PHP
fi
if [ -d "$php_dir/cli/conf.d" ]; then
cat << 'EOF_PHP' > "$php_dir/cli/conf.d/99-nexus.ini"
upload_max_filesize = 512M
post_max_size = 512M
memory_limit = 512M
max_execution_time = 300
max_input_time = 300
EOF_PHP
fi
done
echo "-> Génération des configurations Asterisk..."
php -r '
require_once "/var/www/nexus-pbx/src/Database.php";
require_once "/var/www/nexus-pbx/src/ConfigGenerator.php";
$db = App\Database::getConnection();
$gen = new App\ConfigGenerator($db);
$gen->writeAllConfigs();
' 2>/dev/null || true
echo "-> Redémarrage des services..."
systemctl daemon-reload
systemctl restart php*-fpm 2>/dev/null || true
PHP_SVC=$(systemctl list-unit-files --type=service 2>/dev/null | grep -E "php.*-fpm" | awk '{print $1}' | head -n 1 || echo "")
if [ -n "$PHP_SVC" ]; then
systemctl restart "$PHP_SVC" 2>/dev/null || true
fi
systemctl restart asterisk 2>/dev/null || true
systemctl restart fail2ban 2>/dev/null || true
systemctl restart nginx 2>/dev/null || true
echo ""
echo "=========================================================="
echo " [OK] Réparation terminée ! Toutes les permissions sont actives."
echo "=========================================================="
/var/www/nexus-pbx/scripts/nexus-banner.sh --force || true
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env bash
# Redirection vers le script d'installation principal
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec bash "$DIR/install.sh" "$@"
-1
View File
@@ -1 +0,0 @@
setup-debian.sh
+12 -9
View File
@@ -81,15 +81,18 @@ fi
echo -e "\n${C_YELLOW}-> Mise à jour vers la version v${REMOTE_VER}...${C_RESET}" echo -e "\n${C_YELLOW}-> Mise à jour vers la version v${REMOTE_VER}...${C_RESET}"
# 1. Sauvegarde préventive automatique # 1. Sauvegarde préventive automatique (Disaster Recovery complète)
echo -e "${C_GRAY}[1/6] Création d'une sauvegarde préventive du système...${C_RESET}" echo -e "${C_GRAY}[1/6] Création d'une sauvegarde préventive du système (Disaster Recovery)...${C_RESET}"
mkdir -p "$APP_DIR/storage/backups" php -r '
BACKUP_DATE=$(date +%Y%m%d_%H%M%S) require_once "/var/www/nexus-pbx/src/Database.php";
BACKUP_FILE="$APP_DIR/storage/backups/pre-update-${BACKUP_DATE}.tar.gz" require_once "/var/www/nexus-pbx/src/BackupManager.php";
tar -czf "$BACKUP_FILE" \ try {
-C "$APP_DIR/storage" database.sqlite \ $res = App\BackupManager::createBackup("pre-update");
-C /etc/asterisk pjsip.custom.conf extensions.custom.conf 2>/dev/null || true echo " ✓ Sauvegarde Disaster Recovery créée : " . $res["filename"] . " (" . $res["size_formatted"] . ")\n";
echo -e "${C_GREEN} ✓ Sauvegarde de sécurité créée : $(basename "$BACKUP_FILE")${C_RESET}" } catch (\Throwable $e) {
echo " ! Avertissement sauvegarde préventive : " . $e->getMessage() . "\n";
}
' || true
# 2. Application des derniers fichiers de code Git # 2. Application des derniers fichiers de code Git
echo -e "${C_GRAY}[2/6] Déploiement des nouveaux fichiers de code...${C_RESET}" echo -e "${C_GRAY}[2/6] Déploiement des nouveaux fichiers de code...${C_RESET}"
+36 -5
View File
@@ -11,7 +11,22 @@ use InvalidArgumentException;
class BackupManager class BackupManager
{ {
public const BACKUP_DIR = '/var/www/nexus-pbx/storage/system_backups'; public const BACKUP_DIR = '/var/www/nexus-pbx/storage/system_backups';
public const VERSION = '1.2.0'; public const VERSION = '1.6.2';
/**
* Retourne dynamiquement la version courante de NexusPBX lue depuis version.json
*/
public static function getAppVersion(): string
{
$vFile = dirname(__DIR__) . '/version.json';
if (file_exists($vFile)) {
$data = json_decode((string)file_get_contents($vFile), true);
if (!empty($data['version'])) {
return (string)$data['version'];
}
}
return self::VERSION;
}
/** /**
* Initialise le répertoire des sauvegardes. * Initialise le répertoire des sauvegardes.
@@ -182,7 +197,8 @@ class BackupManager
} }
$timestamp = date('Ymd_His'); $timestamp = date('Ymd_His');
$filename = "nexus_backup_v" . self::VERSION . "_{$timestamp}.tar.gz"; $currentVer = self::getAppVersion();
$filename = "nexus_backup_v{$currentVer}_{$timestamp}.tar.gz";
$localArchivePath = self::BACKUP_DIR . '/' . $filename; $localArchivePath = self::BACKUP_DIR . '/' . $filename;
// 2. Préparation du dossier de staging // 2. Préparation du dossier de staging
@@ -268,7 +284,7 @@ class BackupManager
// G. Création du Manifeste // G. Création du Manifeste
$manifest = [ $manifest = [
'nexus_version' => self::VERSION, 'nexus_version' => $currentVer,
'created_at' => date('c'), 'created_at' => date('c'),
'timestamp' => time(), 'timestamp' => time(),
'filename' => $filename, 'filename' => $filename,
@@ -360,6 +376,14 @@ class BackupManager
// Tenter de lire le manifest depuis l'archive sans l'extraire entièrement // Tenter de lire le manifest depuis l'archive sans l'extraire entièrement
$manifest = self::readManifestFromTar($file); $manifest = self::readManifestFromTar($file);
$archiveVer = $manifest['nexus_version'] ?? null;
if (!$archiveVer && preg_match('/_v([0-9]+\.[0-9]+(?:\.[0-9]+)?)/', $name, $vm)) {
$archiveVer = $vm[1];
}
if (!$archiveVer) {
$archiveVer = self::getAppVersion();
}
$backups[] = [ $backups[] = [
'filename' => $name, 'filename' => $name,
'storage_type' => 'local', 'storage_type' => 'local',
@@ -367,7 +391,7 @@ class BackupManager
'size_formatted' => self::formatBytes($size), 'size_formatted' => self::formatBytes($size),
'timestamp' => $mtime, 'timestamp' => $mtime,
'date_formatted' => date('d/m/Y H:i:s', $mtime), 'date_formatted' => date('d/m/Y H:i:s', $mtime),
'version' => $manifest['nexus_version'] ?? self::VERSION, 'version' => $archiveVer,
'source' => $manifest['source'] ?? 'local', 'source' => $manifest['source'] ?? 'local',
'is_local' => true, 'is_local' => true,
]; ];
@@ -807,6 +831,13 @@ class BackupManager
$name = basename($fullPath); $name = basename($fullPath);
if (str_ends_with($name, '.tar.gz')) { if (str_ends_with($name, '.tar.gz')) {
$remoteVer = null;
if (preg_match('/_v([0-9]+\.[0-9]+(?:\.[0-9]+)?)/', $name, $vm)) {
$remoteVer = $vm[1];
}
if (!$remoteVer) {
$remoteVer = self::getAppVersion();
}
$results[] = [ $results[] = [
'filename' => $name, 'filename' => $name,
'storage_type' => 'ssh', 'storage_type' => 'ssh',
@@ -814,7 +845,7 @@ class BackupManager
'size_formatted' => self::formatBytes($size), 'size_formatted' => self::formatBytes($size),
'timestamp' => $timestamp > 0 ? $timestamp : time(), 'timestamp' => $timestamp > 0 ? $timestamp : time(),
'date_formatted' => date('d/m/Y H:i:s', $timestamp > 0 ? $timestamp : time()), 'date_formatted' => date('d/m/Y H:i:s', $timestamp > 0 ? $timestamp : time()),
'version' => self::VERSION, 'version' => $remoteVer,
'source' => 'distant (SSH)', 'source' => 'distant (SSH)',
'is_local' => false, 'is_local' => false,
]; ];
+101 -7
View File
@@ -67,6 +67,25 @@ class ConfigGenerator
$lines[] = "; ==========================================================================="; $lines[] = "; ===========================================================================";
$lines[] = ""; $lines[] = "";
$rtpTimeout = max(5, (int)($settings['rtp_timeout'] ?? 30));
$rtpHoldTimeout = max(0, (int)($settings['rtp_timeout_hold'] ?? 0));
$rtpKeepalive = max(0, (int)($settings['rtp_keepalive'] ?? 15));
$tcpKeepaliveLines = [
"tcp_keepalive_enable = yes",
"tcp_keepalive_idle_time = 20",
"tcp_keepalive_interval_time = 5",
"tcp_keepalive_probe_count = 3",
];
// 0. Paramètres globaux PJSIP (Keepalive applicatif TLS/TCP toutes les 30s pour postes hors réseau / NAT)
$lines[] = "; --- PARAMÈTRES GLOBAUX PJSIP (KEEPALIVE NAT / HORS RÉSEAU) ---";
$lines[] = "[global]";
$lines[] = "type = global";
$lines[] = "keep_alive_interval = 30";
$lines[] = "contact_expiration_check_interval = 15";
$lines[] = "";
// 1. Transport UDP (Postes internes LAN & Trunks standards) // 1. Transport UDP (Postes internes LAN & Trunks standards)
$lines[] = "; --- TRANSPORT UDP (LAN / INTERNE) ---"; $lines[] = "; --- TRANSPORT UDP (LAN / INTERNE) ---";
$lines[] = "[transport-udp]"; $lines[] = "[transport-udp]";
@@ -110,6 +129,7 @@ class ConfigGenerator
$lines[] = "external_signaling_port = {$extUdpPort}"; $lines[] = "external_signaling_port = {$extUdpPort}";
} }
} }
array_push($lines, ...$tcpKeepaliveLines);
$lines[] = "allow_reload = no"; $lines[] = "allow_reload = no";
$lines[] = ""; $lines[] = "";
@@ -180,6 +200,7 @@ class ConfigGenerator
$lines[] = "external_signaling_port = {$extTlsPort}"; $lines[] = "external_signaling_port = {$extTlsPort}";
} }
} }
array_push($lines, ...$tcpKeepaliveLines);
$lines[] = "allow_reload = no"; $lines[] = "allow_reload = no";
$lines[] = ""; $lines[] = "";
@@ -221,6 +242,15 @@ class ConfigGenerator
$lines[] = "force_rport = yes"; $lines[] = "force_rport = yes";
$lines[] = "rewrite_contact = yes"; $lines[] = "rewrite_contact = yes";
$lines[] = "direct_media = no"; $lines[] = "direct_media = no";
// Détection automatique et purge des flux RTP fantômes / morts (coupure réseau, NAT hors réseau)
$lines[] = "rtp_timeout = {$rtpTimeout}";
$lines[] = "rtp_timeout_hold = {$rtpHoldTimeout}";
if ($rtpKeepalive > 0) {
$lines[] = "rtp_keepalive = {$rtpKeepalive}";
}
$lines[] = "timers = yes";
$lines[] = "timers_min_se = 90";
$lines[] = "timers_sess_expires = 1800";
if ($wantsTls && $hasTlsCerts) { if ($wantsTls && $hasTlsCerts) {
// Verrouillage strict du transport TLS : refuse le repli sur UDP // Verrouillage strict du transport TLS : refuse le repli sur UDP
$lines[] = "transport = transport-tls"; $lines[] = "transport = transport-tls";
@@ -295,13 +325,14 @@ class ConfigGenerator
} }
$context = trim((string)($trunk['context'] ?? 'from-external')) ?: 'from-external'; $context = trim((string)($trunk['context'] ?? 'from-external')) ?: 'from-external';
$fromUser = trim((string)($trunk['from_user'] ?? '')) ?: $username; $fromUser = trim((string)($trunk['from_user'] ?? ''));
$fromDomain = trim((string)($trunk['from_domain'] ?? '')) ?: $host; $fromDomain = trim((string)($trunk['from_domain'] ?? '')) ?: $host;
$callerId = trim((string)($trunk['outbound_callerid'] ?? '')); $callerId = trim((string)($trunk['outbound_callerid'] ?? ''));
$sendPai = ((int)($trunk['send_pai'] ?? 1) === 1) ? 'yes' : 'no'; $sendPai = ((int)($trunk['send_pai'] ?? 1) === 1) ? 'yes' : 'no';
$sendRpid = ((int)($trunk['send_rpid'] ?? 0) === 1) ? 'yes' : 'no'; $sendRpid = ((int)($trunk['send_rpid'] ?? 0) === 1) ? 'yes' : 'no';
$sendDiversion = ((int)($trunk['send_diversion'] ?? 1) === 1) ? 'yes' : 'no'; $sendDiversion = ((int)($trunk['send_diversion'] ?? 1) === 1) ? 'yes' : 'no';
$trustInbound = ((int)($trunk['trust_id_inbound'] ?? 1) === 1) ? 'yes' : 'no'; $trustInbound = ((int)($trunk['trust_id_inbound'] ?? 1) === 1) ? 'yes' : 'no';
$trustOutbound = (!isset($trunk['trust_id_outbound']) || (int)$trunk['trust_id_outbound'] === 1) ? 'yes' : 'no';
$dtmfMode = trim((string)($trunk['dtmf_mode'] ?? 'rfc4733')) ?: 'rfc4733'; $dtmfMode = trim((string)($trunk['dtmf_mode'] ?? 'rfc4733')) ?: 'rfc4733';
$mediaEnc = trim((string)($trunk['media_encryption'] ?? 'no')) ?: 'no'; $mediaEnc = trim((string)($trunk['media_encryption'] ?? 'no')) ?: 'no';
$natSym = ((int)($trunk['nat_symmetric'] ?? 1) === 1) ? 'yes' : 'no'; $natSym = ((int)($trunk['nat_symmetric'] ?? 1) === 1) ? 'yes' : 'no';
@@ -353,9 +384,10 @@ class ConfigGenerator
if ($outboundProxy !== '') { if ($outboundProxy !== '') {
$lines[] = "outbound_proxy = {$outboundProxy}"; $lines[] = "outbound_proxy = {$outboundProxy}";
} }
if ($callerId !== '') { // Note : Ne JAMAIS positionner la directive "callerid" dans la section endpoint d'un Trunk PJSIP.
$lines[] = "callerid = {$callerId}"; // Dans Asterisk, "callerid" sur un endpoint sert de valeur par défaut pour les appels ENTRANTS,
} // ce qui écraserait le numéro de l'appelant externe (ex: mobile 06...).
// Le CallerID sortant du Trunk (outbound_callerid) est appliqué uniquement dans le Dialplan sortant.
$lines[] = "disallow = all"; $lines[] = "disallow = all";
foreach ($codecs as $codec) { foreach ($codecs as $codec) {
$lines[] = "allow = {$codec}"; $lines[] = "allow = {$codec}";
@@ -370,7 +402,10 @@ class ConfigGenerator
$lines[] = "send_rpid = {$sendRpid}"; $lines[] = "send_rpid = {$sendRpid}";
$lines[] = "send_diversion = {$sendDiversion}"; $lines[] = "send_diversion = {$sendDiversion}";
$lines[] = "trust_id_inbound = {$trustInbound}"; $lines[] = "trust_id_inbound = {$trustInbound}";
$lines[] = "trust_id_outbound = {$trustOutbound}";
$lines[] = "timers = {$timers}"; $lines[] = "timers = {$timers}";
$lines[] = "rtp_timeout = {$rtpTimeout}";
$lines[] = "rtp_timeout_hold = {$rtpHoldTimeout}";
if ($rtpKeepalive > 0) { if ($rtpKeepalive > 0) {
$lines[] = "rtp_keepalive = {$rtpKeepalive}"; $lines[] = "rtp_keepalive = {$rtpKeepalive}";
} }
@@ -1158,7 +1193,9 @@ class ConfigGenerator
// 3. Positionnement du CallerID Sortant de la route // 3. Positionnement du CallerID Sortant de la route
if ($rCid !== '') { if ($rCid !== '') {
$sub[] = " same => n,Set(CALLERID(all)={$rCid})"; foreach ($this->buildCallerIdDialplan($rCid) as $cidStmt) {
$sub[] = $cidStmt;
}
} }
// 4. Séquence de Trunks avec bascule automatique (Failover) // 4. Séquence de Trunks avec bascule automatique (Failover)
@@ -1172,8 +1209,10 @@ class ConfigGenerator
$tSlug = $tObj ? $this->slugify($tObj['name']) : $this->slugify($tKey); $tSlug = $tObj ? $this->slugify($tObj['name']) : $this->slugify($tKey);
$tFallbackCid = $tObj ? trim((string)($tObj['outbound_callerid'] ?? '')) : ''; $tFallbackCid = $tObj ? trim((string)($tObj['outbound_callerid'] ?? '')) : '';
if ($rCid === '' && $tFallbackCid !== '' && $overrideCid) { if ($rCid === '' && $tFallbackCid !== '') {
$sub[] = " same => n,Set(CALLERID(all)={$tFallbackCid})"; foreach ($this->buildCallerIdDialplan($tFallbackCid) as $cidStmt) {
$sub[] = $cidStmt;
}
} }
$validTrunkCount++; $validTrunkCount++;
@@ -2257,5 +2296,60 @@ class ConfigGenerator
$slug = preg_replace('/[^a-z0-9]+/', '-', $slug); $slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
return trim($slug, '-') ?: 'default'; return trim($slug, '-') ?: 'default';
} }
/**
* Formate les instructions Dialplan pour le positionnement du Caller ID (Nom, Numéro, Présentation, CallerID all)
* afin de garantir la transmission du CNAM (Caller ID Name) et du CLI par Asterisk PJSIP.
*
* @param string $cidString Chaîne du type '"Sytelis Consulting" <0422560185>', 'Sytelis <0422560185>', '0422560185' ou 'Sytelis'
* @param string $indent
* @return array<string>
*/
private function buildCallerIdDialplan(string $cidString, string $indent = ' same => n,'): array
{
$cidString = trim($cidString);
if ($cidString === '') {
return [];
}
$name = '';
$num = '';
// Formats reconnus :
// 1. "Nom Complet" <0422560185> ou Nom Complet <0422560185>
if (preg_match('/^(?:\"?([^\"<>]+?)\"?\s*)?<([^>]+)>$/u', $cidString, $m)) {
$name = trim($m[1] ?? '');
$num = trim($m[2] ?? '');
} elseif (preg_match('/^<([^>]+)>$/', $cidString, $m)) {
$num = trim($m[1]);
} elseif (preg_match('/^\+?[0-9*#]+$/', $cidString)) {
$num = $cidString;
} else {
// Uniquement un nom
$name = trim($cidString, " \t\n\r\0\x0B\"");
}
$lines = [];
if ($num !== '') {
$lines[] = "{$indent}Set(CALLERID(num)={$num})";
$lines[] = "{$indent}Set(CALLERID(num-pres)=allowed_passed_screen)";
}
if ($name !== '') {
$cleanName = str_replace('"', '', $name);
$lines[] = "{$indent}Set(CALLERID(name)=\"{$cleanName}\")";
$lines[] = "{$indent}Set(CALLERID(name-pres)=allowed_passed_screen)";
}
if ($name !== '' && $num !== '') {
$cleanName = str_replace('"', '', $name);
$lines[] = "{$indent}Set(CALLERID(all)=\"{$cleanName}\" <{$num}>)";
} elseif ($num !== '') {
$lines[] = "{$indent}Set(CALLERID(all)={$num})";
} elseif ($name !== '') {
$cleanName = str_replace('"', '', $name);
$lines[] = "{$indent}Set(CALLERID(all)=\"{$cleanName}\")";
}
return $lines;
}
} }
+2
View File
@@ -150,6 +150,7 @@ class Database
send_pai INTEGER NOT NULL DEFAULT 1 CHECK (send_pai IN (0, 1)), send_pai INTEGER NOT NULL DEFAULT 1 CHECK (send_pai IN (0, 1)),
send_rpid INTEGER NOT NULL DEFAULT 0 CHECK (send_rpid IN (0, 1)), send_rpid INTEGER NOT NULL DEFAULT 0 CHECK (send_rpid IN (0, 1)),
trust_id_inbound INTEGER NOT NULL DEFAULT 1 CHECK (trust_id_inbound IN (0, 1)), trust_id_inbound INTEGER NOT NULL DEFAULT 1 CHECK (trust_id_inbound IN (0, 1)),
trust_id_outbound INTEGER NOT NULL DEFAULT 1 CHECK (trust_id_outbound IN (0, 1)),
codecs VARCHAR(64) NOT NULL DEFAULT 'alaw,ulaw,g722', codecs VARCHAR(64) NOT NULL DEFAULT 'alaw,ulaw,g722',
dtmf_mode VARCHAR(16) NOT NULL DEFAULT 'rfc4733', dtmf_mode VARCHAR(16) NOT NULL DEFAULT 'rfc4733',
media_encryption VARCHAR(16) NOT NULL DEFAULT 'no', media_encryption VARCHAR(16) NOT NULL DEFAULT 'no',
@@ -525,6 +526,7 @@ class Database
'moh_sound_id' => "INTEGER NOT NULL DEFAULT 0", 'moh_sound_id' => "INTEGER NOT NULL DEFAULT 0",
'cid_name_prefix' => "VARCHAR(64) NOT NULL DEFAULT ''", 'cid_name_prefix' => "VARCHAR(64) NOT NULL DEFAULT ''",
'dial_options' => "VARCHAR(32) NOT NULL DEFAULT 'tTR'", 'dial_options' => "VARCHAR(32) NOT NULL DEFAULT 'tTR'",
'trust_id_outbound' => "INTEGER NOT NULL DEFAULT 1 CHECK (trust_id_outbound IN (0, 1))",
]; ];
foreach ($extraColumns as $colName => $colDef) { foreach ($extraColumns as $colName => $colDef) {
+14 -10
View File
@@ -1,16 +1,20 @@
{ {
"version": "1.6.0", "version": "1.6.6",
"codename": "Helios", "codename": "Helios",
"release_date": "2026-11-04", "release_date": "2026-11-10",
"min_php_version": "8.2", "min_php_version": "8.2",
"changelog": [ "changelog": [
"Nouveau module Journal d'Appels & CDR : traçabilité complète des flux entrants, sortants, internes et services avec détection automatique des Trunks SIP et postes associés", "PJSIP Endpoints & Trunks : injection effective de rtp_timeout, rtp_timeout_hold, rtp_keepalive et SIP Session Timers (RFC 4028) sur chaque endpoint PJSIP pour raccrocher instantanément les canaux fantômes",
"Filtres multicritères CDR en temps réel (période, direction, statut décroché/manqué, recherche textuelle) et export CSV Excel", "PJSIP Global & Transports TLS/TCP : keepalive applicatif réduit à 30s (keep_alive_interval = 30), vérification expiration contacts toutes les 15s et TCP keepalives noyau (20s/5s/3) pour fiabiliser les BLF et postes hors réseau derrière NAT",
"Optimisation TLS SIP PJSIP : négociation universelle TLSv1.2/TLSv1.3 (method=sslv23)", "Trunks SIP : dissociation stricte du CallerID sortant du Trunk (suppression de callerid dans l'endpoint PJSIP pour préserver l'affichage du numéro appelant réel sur les appels entrants)",
"Suppression de CertificateRequest en mode TLS standard pour compatibilité totale avec les téléphones IP Yealink, Fanvil et Grandstream", "Trunks SIP : suppression du forçage de from_user afin de préserver l'en-tête SIP From dynamique avec Nom d'appelant (CNAM) pour OVH / Orange",
"Correction de l'émission et du renouvellement Let's Encrypt / Certbot ACME (privilèges sudo et permissions d'accès aux certificats)", "Trunks SIP & Routage Sortant : activation systématique de trust_id_outbound et formatage strict du Caller ID Name (CNAM) pour affichage garanti chez les opérateurs distants (OVH, Orange, etc.)",
"Plafond d'expiration d'enregistrement SIP standard (maximum_expiration = 300s, default = 120s) garantissant la reconnexion automatique rapide de tous les postes (1 à 3 min)", "Sauvegarde & Restauration : versionnage dynamique automatique basé sur la version installée (résolution du tag figé v1.2.0)",
"Nettoyage et isolation stricte des sauvegardes (base NexusPBX, sons, spools voicemail, certificats)", "Mises à jour système OTA : déclenchement garanti d'une sauvegarde Disaster Recovery complète pré-mise à jour (visible et restaurable dans l'interface)",
"Résolution définitive et propre des conflits de préfixes Dialplan sur les SVI/IVR (*801-*899)" "Interface Sauvegardes : nouveau badge identifiant la source (Pré-MàJ, Auto, Manuel)",
"Identité visuelle officielle : intégration du nouveau logo NexusPBX 3D haute définition épuré et du favicon",
"Nouveau slogan télécom : « Supervision & Téléphonie IP • Powered by Asterisk »",
"Module Journal d'Appels & CDR avec filtres et export CSV",
"Support universel TLSv1.2/TLSv1.3 et suppression CertificateRequest pour postes Yealink"
] ]
} }