feat: add support for inbound trunk registration (Receive mode) for FXO gateways like Grandstream HT813 and analog Livebox integration

This commit is contained in:
sorcier committed 2026-11-03 19:07:19 +01:00
1 parent 981c650c7a
commit 8a4d7a0504
5 files changed
+274 -26

No files matched your search

+29 -8
View File
@@ -1086,7 +1086,15 @@ try {
$username = trim((string)($payload['username'] ?? ''));
$auth_username = trim((string)($payload['auth_username'] ?? ''));
$secret = trim((string)($payload['secret'] ?? ''));
$register = ($auth_mode === 'ip') ? 0 : (int)!empty($payload['register']);
$rawRegMode = strtolower(trim((string)($payload['registration_mode'] ?? '')));
if (in_array($rawRegMode, ['send', 'receive', 'none'], true)) {
$registration_mode = $rawRegMode;
} else {
$registration_mode = (!empty($payload['register']) && $auth_mode !== 'ip') ? 'send' : 'none';
}
$register = ($registration_mode === 'send') ? 1 : 0;
$server_uri = trim((string)($payload['server_uri'] ?? ''));
$client_uri = trim((string)($payload['client_uri'] ?? ''));
$contact_user = trim((string)($payload['contact_user'] ?? ''));
@@ -1130,8 +1138,17 @@ try {
$cleanDidRoutes = ConfigGenerator::normalizeInboundRoutes(json_encode($didRoutesRaw));
$did_routes_json = json_encode($cleanDidRoutes, JSON_UNESCAPED_UNICODE);
if ($name === '' || $host === '') {
throw new InvalidArgumentException("Le nom du Trunk et l'hôte SIP opérateur sont requis.");
if ($name === '') {
throw new InvalidArgumentException("Le nom du Trunk est requis.");
}
if ($registration_mode === 'receive') {
if ($username === '' || $secret === '') {
throw new InvalidArgumentException("En mode Réception (ex: passerelle Grandstream HT813 / Livebox), l'Identifiant (Username) et le Mot de passe SIP sont requis pour authentifier la passerelle.");
}
} else {
if ($host === '') {
throw new InvalidArgumentException("L'hôte SIP opérateur (Serveur SIP) est requis.");
}
}
$params = [
@@ -1144,6 +1161,7 @@ try {
':username' => $username,
':auth_username' => $auth_username,
':secret' => $secret,
':registration_mode' => $registration_mode,
':register' => $register,
':server_uri' => $server_uri,
':client_uri' => $client_uri,
@@ -1192,7 +1210,8 @@ try {
UPDATE trunks SET
name = :name, host = :host, port = :port, outbound_proxy = :outbound_proxy,
transport = :transport, auth_mode = :auth_mode, username = :username,
auth_username = :auth_username, secret = :secret, register = :register,
auth_username = :auth_username, secret = :secret,
registration_mode = :registration_mode, register = :register,
server_uri = :server_uri, client_uri = :client_uri, contact_user = :contact_user,
expiration = :expiration, retry_interval = :retry_interval,
max_retries = :max_retries, forbidden_retry_interval = :forbidden_retry_interval,
@@ -1217,7 +1236,7 @@ try {
$stmt = $db->prepare('
INSERT INTO trunks (
name, host, port, outbound_proxy, transport, auth_mode, username, auth_username, secret,
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,
from_user, from_domain, outbound_callerid, send_pai, send_rpid, send_diversion,
trust_id_inbound, codecs, dtmf_mode, media_encryption, nat_symmetric, direct_media,
@@ -1226,7 +1245,7 @@ try {
outbound_prefix, custom_endpoint_params, custom_reg_params, custom_aor_params, moh_sound_id, enabled
) VALUES (
:name, :host, :port, :outbound_proxy, :transport, :auth_mode, :username, :auth_username, :secret,
: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,
:from_user, :from_domain, :outbound_callerid, :send_pai, :send_rpid, :send_diversion,
:trust_id_inbound, :codecs, :dtmf_mode, :media_encryption, :nat_symmetric, :direct_media,
@@ -1237,7 +1256,8 @@ try {
ON CONFLICT(name) DO UPDATE SET
host = excluded.host, port = excluded.port, outbound_proxy = excluded.outbound_proxy,
transport = excluded.transport, auth_mode = excluded.auth_mode, username = excluded.username,
auth_username = excluded.auth_username, secret = excluded.secret, register = excluded.register,
auth_username = excluded.auth_username, secret = excluded.secret,
registration_mode = excluded.registration_mode, register = excluded.register,
server_uri = excluded.server_uri, client_uri = excluded.client_uri, contact_user = excluded.contact_user,
expiration = excluded.expiration, retry_interval = excluded.retry_interval,
max_retries = excluded.max_retries, forbidden_retry_interval = excluded.forbidden_retry_interval,
@@ -1259,9 +1279,10 @@ try {
$stmt->execute($params);
}
$displayHost = $host !== '' ? "{$host}:{$port}" : "Passerelle locale / HT813";
echo json_encode([
'success' => true,
'message' => "Trunk SIP « {$name} » ({$host}:{$port}) sauvegardé avec tous ses paramètres PJSIP.",
'message' => "Trunk SIP « {$name} » ({$displayHost}) sauvegardé avec tous ses paramètres PJSIP.",
]);
break;
+110 -5
View File
@@ -1579,14 +1579,25 @@ function renderTrunksTable() {
tbody.innerHTML = rows.map(t => {
const slug = `trunk-${slugifyTrunkName(t.name)}`;
const regInfo = regs[slug];
const regMode = t.registration_mode || (Number(t.register) === 1 ? 'send' : 'none');
const isIpAuth = t.auth_mode === 'ip';
const wantsReg = !isIpAuth && Number(t.register) === 1;
let statusBadge = '';
if (Number(t.enabled ?? 1) === 0) {
statusBadge = `<span class="px-2.5 py-0.5 rounded-full text-xs font-medium bg-anthracite-950 text-slate-400 border border-anthracite-700">Désactivé</span>`;
} else if (isIpAuth || !wantsReg) {
statusBadge = `<span class="px-2.5 py-0.5 rounded-full text-xs font-semibold bg-cyan-500/15 text-cyan-300 border border-cyan-500/30">Peering / Sans Reg.</span>`;
} else if (regMode === 'receive') {
const contacts = appState.metrics?.asterisk?.contacts || [];
const isOnline = contacts.some(c =>
(c.extension === slug || c.extension === t.username || (t.username && c.uri && c.uri.includes(t.username)))
&& String(c.status || '').toLowerCase().includes('avail')
);
if (isOnline) {
statusBadge = `<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/30"><i class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></i>Passerelle En ligne</span>`;
} else {
statusBadge = `<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-semibold bg-amber-500/20 text-amber-300 border border-amber-500/30">En attente passerelle (Receive)</span>`;
}
} else if (isIpAuth || regMode === 'none') {
statusBadge = `<span class="px-2.5 py-0.5 rounded-full text-xs font-semibold bg-purple-500/15 text-purple-300 border border-purple-500/30">Peering IP direct</span>`;
} else if (regInfo && /Registered/i.test(regInfo.status)) {
statusBadge = `<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/30"><i class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></i>Registered</span>`;
} else if (regInfo) {
@@ -1618,7 +1629,9 @@ function renderTrunksTable() {
</td>
<td class="py-3.5 px-4">${statusBadge}</td>
<td class="py-3.5 px-4">
<div class="font-mono text-xs text-indigo-300">${escapeHtml(t.host)}:${Number(t.port)}</div>
<div class="font-mono text-xs ${t.host ? 'text-indigo-300' : 'text-emerald-300'}">
${t.host ? escapeHtml(t.host) + ':' + Number(t.port || 5060) : 'Passerelle locale (Dynamique)'}
</div>
${t.outbound_proxy ? `<div class="text-[11px] font-mono text-slate-400 truncate max-w-[180px]">Proxy: ${escapeHtml(t.outbound_proxy)}</div>` : ''}
</td>
<td class="py-3.5 px-4">
@@ -2085,6 +2098,57 @@ function parseFreePbxConfig() {
}
}
function onTrunkRegistrationModeChange(mode) {
const form = document.getElementById('form-trunk');
if (!form) return;
const hostInput = form.elements['host'];
const hostLabel = document.getElementById('trunk-host-label');
const hostHint = document.getElementById('trunk-host-hint');
const receiveGuide = document.getElementById('trunk-receive-guide');
const regNotice = document.getElementById('trunk-reg-outbound-notice');
if (mode === 'receive') {
if (hostInput) {
hostInput.required = false;
hostInput.placeholder = 'Optionnel (vide = IP découverte dynamiquement au REGISTER du HT813)';
}
if (hostLabel) {
hostLabel.innerHTML = 'Serveur SIP / IP Passerelle <span class="text-emerald-400 font-semibold">(Optionnel en mode Réception)</span>';
}
hostHint?.classList.remove('hidden');
receiveGuide?.classList.remove('hidden');
regNotice?.classList.remove('hidden');
if (form.elements['register']) form.elements['register'].checked = false;
if (form.elements['line_support']) form.elements['line_support'].checked = false;
} else if (mode === 'none') {
if (hostInput) {
hostInput.required = true;
hostInput.placeholder = 'ex: 192.168.1.250 ou sbc.direct.net';
}
if (hostLabel) {
hostLabel.innerHTML = 'Serveur SIP / SBC / Passerelle * <span class="text-slate-400">(Host / IP)</span>';
}
hostHint?.classList.add('hidden');
receiveGuide?.classList.add('hidden');
regNotice?.classList.remove('hidden');
if (form.elements['register']) form.elements['register'].checked = false;
} else {
// mode === 'send'
if (hostInput) {
hostInput.required = true;
hostInput.placeholder = 'ex: sbc.operateur.net ou siptrunk.ovh.net';
}
if (hostLabel) {
hostLabel.innerHTML = 'Serveur SIP / SBC / Passerelle * <span class="text-slate-400">(Host / IP)</span>';
}
hostHint?.classList.add('hidden');
receiveGuide?.classList.add('hidden');
regNotice?.classList.add('hidden');
if (form.elements['register']) form.elements['register'].checked = true;
}
}
function applyTrunkArchitecture(arch) {
const form = document.getElementById('form-trunk');
if (!form) return;
@@ -2095,17 +2159,31 @@ function applyTrunkArchitecture(arch) {
port: 5060,
transport: 'udp',
auth_mode: 'userpass',
registration_mode: 'send',
register: true,
line_support: true,
media_encryption: 'no',
nat_symmetric: true,
qualify_frequency: 60
},
gateway_receive: {
label: 'Passerelle FXO / Analogique (Grandstream HT813, Livebox, ATA)',
port: 5060,
transport: 'udp',
auth_mode: 'userpass',
registration_mode: 'receive',
register: false,
line_support: false,
media_encryption: 'no',
nat_symmetric: true,
qualify_frequency: 60
},
ip_peering: {
label: 'Peering IP / SBC / Passerelle (Sans REGISTER)',
port: 5060,
transport: 'udp',
auth_mode: 'ip',
registration_mode: 'none',
register: false,
line_support: false,
media_encryption: 'no',
@@ -2117,6 +2195,7 @@ function applyTrunkArchitecture(arch) {
port: 5061,
transport: 'tls',
auth_mode: 'userpass',
registration_mode: 'send',
register: true,
line_support: true,
media_encryption: 'sdes',
@@ -2134,6 +2213,13 @@ function applyTrunkArchitecture(arch) {
form.querySelectorAll('input[name="auth_mode"]').forEach(r => { r.checked = (r.value === a.auth_mode); });
toggleTrunkAuthFields(a.auth_mode);
if (a.registration_mode) {
form.querySelectorAll('input[name="registration_mode"]').forEach(r => {
r.checked = (r.value === a.registration_mode);
});
onTrunkRegistrationModeChange(a.registration_mode);
}
form.elements['register'].checked = a.register;
form.elements['line_support'].checked = a.line_support;
form.elements['media_encryption'].value = a.media_encryption;
@@ -2152,11 +2238,18 @@ function openTrunkModal(trunk = null) {
form.elements['id'].value = trunk ? trunk.id : '';
form.elements['name'].value = trunk ? trunk.name : '';
form.elements['host'].value = trunk ? trunk.host : '';
form.elements['host'].value = trunk ? (trunk.host ?? '') : '';
form.elements['port'].value = trunk ? (trunk.port ?? 5060) : 5060;
form.elements['outbound_proxy'].value = trunk ? (trunk.outbound_proxy ?? '') : '';
form.elements['transport'].value = trunk ? (trunk.transport ?? 'udp') : 'udp';
const regMode = trunk ? (trunk.registration_mode || (Number(trunk.register) === 1 ? 'send' : 'none')) : 'send';
form.querySelectorAll('input[name="registration_mode"]').forEach(r => {
r.checked = (r.value === regMode);
});
onTrunkRegistrationModeChange(regMode);
form.elements['transport'].value = trunk ? (trunk.transport ?? 'udp') : 'udp';
const authMode = trunk ? (trunk.auth_mode || 'userpass') : 'userpass';
form.querySelectorAll('input[name="auth_mode"]').forEach(r => {
r.checked = (r.value === authMode);
@@ -2168,6 +2261,17 @@ function openTrunkModal(trunk = null) {
form.elements['secret'].value = trunk ? (trunk.secret ?? '') : '';
form.elements['enabled'].checked = trunk ? Number(trunk.enabled ?? 1) === 1 : true;
const uInput = form.elements['username'];
const updateGuide = () => {
const val = uInput.value.trim() || 'Même valeur que Username';
const gUser = document.getElementById('ht813-guide-user');
const gAuth = document.getElementById('ht813-guide-auth');
if (gUser) gUser.textContent = val;
if (gAuth) gAuth.textContent = val;
};
uInput.oninput = updateGuide;
updateGuide();
// Onglet 2 : Registration & Identité SIP
form.elements['register'].checked = trunk ? Number(trunk.register ?? 1) === 1 : true;
form.elements['line_support'].checked = trunk ? Number(trunk.line_support ?? 1) === 1 : true;
@@ -3583,6 +3687,7 @@ document.addEventListener('DOMContentLoaded', () => {
const fd = new FormData(form);
const payload = Object.fromEntries(fd.entries());
payload.registration_mode = form.querySelector('input[name="registration_mode"]:checked')?.value || 'send';
payload.enabled = form.elements['enabled'].checked ? 1 : 0;
payload.register = form.elements['register'].checked ? 1 : 0;
payload.line_support = form.elements['line_support'].checked ? 1 : 0;
+74 -4
View File
@@ -3278,6 +3278,9 @@ $currentUser = Auth::getUsername();
<button type="button" onclick="applyTrunkArchitecture('register_auth')" class="px-2.5 py-1 rounded-lg bg-anthracite-950 hover:bg-indigo-600/30 border border-anthracite-600 text-indigo-300 font-medium transition">
Trunk avec Enregistrement (REGISTER + Auth)
</button>
<button type="button" onclick="applyTrunkArchitecture('gateway_receive')" class="px-2.5 py-1 rounded-lg bg-anthracite-950 hover:bg-emerald-600/30 border border-emerald-500/50 text-emerald-300 font-medium transition flex items-center gap-1">
<span>📟 Passerelle FXO / Analogique (HT813, Livebox)</span>
</button>
<button type="button" onclick="applyTrunkArchitecture('ip_peering')" class="px-2.5 py-1 rounded-lg bg-anthracite-950 hover:bg-indigo-600/30 border border-anthracite-600 text-purple-300 font-medium transition">
Peering IP / SBC / Passerelle (Sans REGISTER)
</button>
@@ -3340,16 +3343,78 @@ $currentUser = Auth::getUsername();
<!-- SOUS-ONGLET 1 : SERVEUR, AUTHENTIFICATION & ROUTAGE DIRECT -->
<div id="trunk-panel-general" class="trunk-subpanel space-y-4">
<!-- Mode d'Enregistrement SIP (Send / Receive / None) -->
<div class="p-4 rounded-xl bg-anthracite-950 border border-anthracite-700 space-y-3">
<div class="flex flex-wrap items-center justify-between gap-2">
<span class="text-xs font-bold uppercase tracking-wider text-emerald-300">Mode d'Enregistrement SIP (Registration)</span>
<span class="text-[11px] text-slate-400">Définit le sens de l'enregistrement ou le mode peering</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 text-xs">
<label class="flex items-start gap-2.5 p-3 rounded-xl bg-anthracite-900 border border-anthracite-700 cursor-pointer hover:border-indigo-500/50 transition">
<input type="radio" name="registration_mode" value="send" checked onchange="onTrunkRegistrationModeChange(this.value)" class="mt-0.5">
<div>
<div class="text-white font-semibold flex items-center gap-1.5">
<span>Émission (Send)</span>
<span class="text-[10px] px-1.5 py-0.2 rounded bg-indigo-500/20 text-indigo-300">Défaut</span>
</div>
<div class="text-[11px] text-slate-400 mt-0.5">Asterisk s'enregistre vers l'opérateur (OVH, Keyyo...). Serveur SIP requis.</div>
</div>
</label>
<label class="flex items-start gap-2.5 p-3 rounded-xl bg-anthracite-900 border border-emerald-500/40 cursor-pointer hover:border-emerald-500 transition">
<input type="radio" name="registration_mode" value="receive" onchange="onTrunkRegistrationModeChange(this.value)" class="mt-0.5">
<div>
<div class="text-emerald-300 font-semibold flex items-center gap-1.5">
<span>Réception (Receive)</span>
<span class="text-[10px] px-1.5 py-0.2 rounded bg-emerald-500/20 text-emerald-300">Passerelle FXO</span>
</div>
<div class="text-[11px] text-slate-300 mt-0.5">La passerelle s'enregistre sur Asterisk (Grandstream HT813, Livebox). Hôte libre.</div>
</div>
</label>
<label class="flex items-start gap-2.5 p-3 rounded-xl bg-anthracite-900 border border-anthracite-700 cursor-pointer hover:border-purple-500/50 transition">
<input type="radio" name="registration_mode" value="none" onchange="onTrunkRegistrationModeChange(this.value)" class="mt-0.5">
<div>
<div class="text-purple-300 font-semibold flex items-center gap-1.5">
<span>Aucun (None)</span>
<span class="text-[10px] px-1.5 py-0.2 rounded bg-purple-500/20 text-purple-300">Peering IP</span>
</div>
<div class="text-[11px] text-slate-400 mt-0.5">Interconnexion directe sans enregistrement (Trunk IP dédié, SBC). Serveur SIP requis.</div>
</div>
</label>
</div>
<!-- Guide interactif d'aide pour Grandstream HT813 / Livebox FXO -->
<div id="trunk-receive-guide" class="hidden p-3.5 rounded-xl bg-emerald-950/40 border border-emerald-500/40 text-xs text-slate-200 space-y-2">
<div class="flex items-center gap-2 font-bold text-emerald-300 text-xs">
<span>💡 Guide Rapide : Paramétrage de votre Grandstream HT813 / HT503 (FXO)</span>
</div>
<p class="text-[11px] text-slate-300 leading-relaxed">
Dans l'interface web du boîtier HT813 (onglet <strong>FXO Port</strong> relié à la prise téléphone verte de votre Livebox / Box opérateur) :
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 text-[11px] font-mono bg-anthracite-950 p-3 rounded-lg border border-emerald-500/25">
<div>• <strong>Primary SIP Server</strong> : <span class="text-emerald-300 font-bold"><?= htmlspecialchars($_SERVER['SERVER_ADDR'] ?? '10.10.175.204') ?>:5060</span></div>
<div>• <strong>SIP Registration</strong> : <span class="text-emerald-300 font-bold">Yes</span></div>
<div>• <strong>SIP User ID</strong> : <span class="text-indigo-300 font-bold" id="ht813-guide-user">Même valeur que Username</span></div>
<div>• <strong>Authenticate ID</strong> : <span class="text-indigo-300 font-bold" id="ht813-guide-auth">Même valeur que Username</span></div>
<div>• <strong>Authenticate Password</strong> : <span class="text-indigo-300 font-bold">Votre mot de passe secret</span></div>
<div>• <strong>Stage Method (1/2)</strong> : <span class="text-slate-300">1 (Recommandé pour appel direct)</span></div>
<div class="sm:col-span-2 text-slate-300 font-sans border-t border-anthracite-800 pt-2 mt-1">
• <strong>Unconditional Call Forward to VoIP</strong> : Définir <em>User ID</em> = votre numéro (ex: <code class="text-amber-300 font-mono">0494679572</code>) et <em>SIP Server</em> = <code class="text-amber-300 font-mono">ch1:<?= htmlspecialchars($_SERVER['SERVER_ADDR'] ?? '10.10.175.204') ?>:5060;</code>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-4 gap-4">
<div class="sm:col-span-1">
<label class="block text-xs font-medium text-slate-200 mb-1">Nom du Trunk * <span class="text-slate-400">(ID PJSIP)</span></label>
<input type="text" name="name" placeholder="ex: mon-trunk-sip" required
<input type="text" name="name" placeholder="ex: mon-trunk-sip ou 0494679572" required
class="w-full px-3 py-2 rounded-lg bg-anthracite-950 border border-anthracite-700 text-sm text-white font-mono">
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-slate-200 mb-1">Serveur SIP / SBC / Passerelle * <span class="text-slate-400">(Host / IP)</span></label>
<input type="text" name="host" placeholder="ex: sbc.operateur.net ou 192.168.1.250" required oninput="syncTrunkHostDomain(this.value)"
<label id="trunk-host-label" class="block text-xs font-medium text-slate-200 mb-1">Serveur SIP / SBC / Passerelle * <span class="text-slate-400">(Host / IP)</span></label>
<input type="text" name="host" id="trunk-host-input" placeholder="ex: sbc.operateur.net ou 192.168.1.250" required oninput="syncTrunkHostDomain(this.value)"
class="w-full px-3 py-2 rounded-lg bg-anthracite-950 border border-anthracite-700 text-sm text-white font-mono">
<div id="trunk-host-hint" class="hidden text-[10px] text-emerald-400 mt-1">En mode Réception (Receive), l'hôte peut rester vide (découverte dynamique du HT813 lors du REGISTER).</div>
</div>
<div class="sm:col-span-1">
<label class="block text-xs font-medium text-slate-200 mb-1">Port SIP * <span class="text-slate-400">(Port)</span></label>
@@ -3520,8 +3585,13 @@ $currentUser = Auth::getUsername();
</div>
</div>
<!-- SOUS-ONGLET 2 : ENREGISTREMENT (REGISTRATION) & IDENTITÉ SIP -->
<!-- SOUS-ONGLET 3 : ENREGISTREMENT (REGISTRATION) & IDENTITÉ SIP -->
<div id="trunk-panel-identity" class="trunk-subpanel hidden space-y-4">
<div id="trunk-reg-outbound-notice" class="hidden p-3.5 rounded-xl bg-indigo-950/40 border border-indigo-500/40 text-xs text-indigo-200 flex items-center gap-2">
<span>ℹ️</span>
<span>En mode <strong>Réception (Passerelle type HT813)</strong> ou <strong>Peering IP direct (None)</strong>, Asterisk n'émet aucun SIP REGISTER sortant. Les paramètres d'enregistrement ci-dessous ne s'appliquent qu'en mode <strong>Émission (Send)</strong>.</span>
</div>
<div class="p-4 rounded-xl bg-anthracite-950 border border-anthracite-700 space-y-4">
<div class="flex flex-wrap items-center justify-between gap-2">
<span class="text-xs font-bold uppercase tracking-wider text-emerald-300">Paramètres d'Enregistrement SIP (<code class="text-emerald-300">[type = registration]</code>)</span>
+55 -8
View File
@@ -308,15 +308,40 @@ class ConfigGenerator
$codecs = array_filter(array_map('trim', explode(',', (string)($trunk['codecs'] ?? 'alaw,ulaw'))));
$hasAuth = ($authMode !== 'ip' && $username !== '' && $secret !== '');
$lines[] = "; --- Trunk : {$trunk['name']} ({$host}:{$port} [{$trProto}]) ---";
$regMode = trim((string)($trunk['registration_mode'] ?? ''));
if (!in_array($regMode, ['send', 'receive', 'none'], true)) {
$regMode = ((int)($trunk['register'] ?? 1) === 1) ? 'send' : 'none';
}
$matchIpsRaw = trim((string)($trunk['match_ips'] ?? ''));
$hostPortDisplay = $host !== '' ? "{$host}:{$port}" : "Passerelle Réception Dynamique";
$lines[] = "; --- Trunk : {$trunk['name']} ({$hostPortDisplay} [{$trProto}] - Mode: {$regMode}) ---";
$lines[] = "[trunk-{$slug}]";
$lines[] = "type = endpoint";
$lines[] = "context = {$context}";
$lines[] = "transport = {$transport}";
if ($regMode === 'receive') {
// Passerelle locale (ex: Grandstream HT813, Livebox, ATA) s'enregistrant sur Asterisk
$lines[] = "auth = trunk-{$slug}-auth";
$lines[] = "outbound_auth = trunk-{$slug}-auth";
if ($username !== '' && $username !== "trunk-{$slug}") {
$lines[] = "aors = trunk-{$slug},{$username}";
} else {
$lines[] = "aors = trunk-{$slug}";
}
if ($host !== '' || $matchIpsRaw !== '') {
$lines[] = "identify_by = auth_username,username,ip";
} else {
$lines[] = "identify_by = auth_username,username";
}
} else {
if ($hasAuth) {
$lines[] = "outbound_auth = trunk-{$slug}-auth";
}
$lines[] = "aors = trunk-{$slug}";
}
if ($outboundProxy !== '') {
$lines[] = "outbound_proxy = {$outboundProxy}";
}
@@ -377,10 +402,16 @@ class ConfigGenerator
}
$lines[] = "";
// Section AOR du Trunk (nommée trunk-$slug)
// Section AOR du Trunk (nommée trunk-$slug et alias username si mode receive)
$lines[] = "[trunk-{$slug}]";
$lines[] = "type = aor";
if ($regMode === 'receive') {
// En mode réception, l'IP/port de la passerelle est découvert dynamiquement lors du REGISTER
$lines[] = "max_contacts = 1";
$lines[] = "remove_existing = yes";
} else {
$lines[] = "contact = sip:{$host}:{$port}";
}
$lines[] = "qualify_frequency = {$qualifyFreq}";
if ($outboundProxy !== '') {
$lines[] = "outbound_proxy = {$outboundProxy}";
@@ -396,8 +427,21 @@ class ConfigGenerator
}
$lines[] = "";
// AOR supplémentaire pour identifier l'enregistrement par identifiant/numéro (ex: 0494679572)
if ($regMode === 'receive' && $username !== '' && $username !== "trunk-{$slug}") {
$lines[] = "[{$username}]";
$lines[] = "type = aor";
$lines[] = "max_contacts = 1";
$lines[] = "remove_existing = yes";
$lines[] = "qualify_frequency = {$qualifyFreq}";
if ($outboundProxy !== '') {
$lines[] = "outbound_proxy = {$outboundProxy}";
}
$lines[] = "";
}
// Section Auth du Trunk
if ($hasAuth) {
if ($hasAuth || $regMode === 'receive') {
$lines[] = "[trunk-{$slug}-auth]";
$lines[] = "type = auth";
$lines[] = "auth_type = userpass";
@@ -406,8 +450,8 @@ class ConfigGenerator
$lines[] = "";
}
// Section Registration du Trunk
if ((int)($trunk['register'] ?? 1) === 1 && $hasAuth) {
// Section Registration du Trunk (uniquement en mode émission 'send')
if ($regMode === 'send' && (int)($trunk['register'] ?? 1) === 1 && $hasAuth) {
$serverUri = trim((string)($trunk['server_uri'] ?? '')) ?: "sip:{$host}:{$port}";
$clientUri = trim((string)($trunk['client_uri'] ?? '')) ?: "sip:{$username}@{$fromDomain}:{$port}";
$contactUser = trim((string)($trunk['contact_user'] ?? ''));
@@ -453,12 +497,13 @@ class ConfigGenerator
$lines[] = "";
}
// Section Identify (Reconnaissance d'IPs / sous-réseaux opérateur)
$matchIpsRaw = trim((string)($trunk['match_ips'] ?? ''));
// Section Identify (Reconnaissance d'IPs / sous-réseaux opérateur ou passerelle)
if ($matchIpsRaw !== '' || ($regMode !== 'receive' && $host !== '') || ($regMode === 'receive' && $host !== '')) {
$matchList = $matchIpsRaw !== ''
? array_filter(array_map('trim', preg_split('/[\s,;]+/', $matchIpsRaw) ?: []))
: [$host];
: ($host !== '' ? [$host] : []);
if (!empty($matchList)) {
$lines[] = "[trunk-{$slug}-identify]";
$lines[] = "type = identify";
$lines[] = "endpoint = trunk-{$slug}";
@@ -468,6 +513,8 @@ class ConfigGenerator
$lines[] = "";
}
}
}
}
return implode("\n", $lines) . "\n";
}
+6 -1
View File
@@ -128,7 +128,7 @@ class Database
CREATE TABLE IF NOT EXISTS trunks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(64) NOT NULL UNIQUE,
host VARCHAR(128) NOT NULL,
host VARCHAR(128) NOT NULL DEFAULT '',
port INTEGER NOT NULL DEFAULT 5060,
outbound_proxy VARCHAR(128) NOT NULL DEFAULT '',
transport VARCHAR(8) NOT NULL DEFAULT 'udp',
@@ -136,6 +136,7 @@ class Database
username VARCHAR(64) NOT NULL DEFAULT '',
auth_username VARCHAR(64) NOT NULL DEFAULT '',
secret VARCHAR(128) NOT NULL DEFAULT '',
registration_mode VARCHAR(16) NOT NULL DEFAULT 'send' CHECK (registration_mode IN ('send', 'receive', 'none')),
register INTEGER NOT NULL DEFAULT 1 CHECK (register IN (0, 1)),
server_uri VARCHAR(192) NOT NULL DEFAULT '',
client_uri VARCHAR(192) NOT NULL DEFAULT '',
@@ -561,6 +562,7 @@ class Database
// Colonnes universelles PJSIP supplémentaires (Session Timers, Fax T.38, Diversion, Retries, Langue, Keepalive)
$extraColumns = [
'registration_mode' => "VARCHAR(16) NOT NULL DEFAULT 'send'",
'max_retries' => "INTEGER NOT NULL DEFAULT 10000",
'forbidden_retry_interval' => "INTEGER NOT NULL DEFAULT 600",
'fatal_retry_interval' => "INTEGER NOT NULL DEFAULT 0",
@@ -579,6 +581,9 @@ class Database
foreach ($extraColumns as $colName => $colDef) {
if (!in_array($colName, $existing, true)) {
$db->exec("ALTER TABLE trunks ADD COLUMN {$colName} {$colDef}");
if ($colName === 'registration_mode') {
$db->exec("UPDATE trunks SET registration_mode = CASE WHEN register = 1 THEN 'send' ELSE 'none' END");
}
}
}
}