calls: limit outside calls at once, per extension and in total
ci / check (push) Successful in 1m16s
ci / images (push) Successful in 22s

Owner's decision after the M11-T8 review: nothing capped how many calls a stolen password could
run at once. Settings → Security: calls.maxPerExtension (3) and calls.maxTotal (10), 0 = no
limit. Each sp-route-* counts the caller's extension (or whose forward it is: SP_LIMITEXT from
sp-sub-forward) with GROUP(spext) and every outside call with GROUP(spout); over a limit the
caller hears "all circuits are busy". 911 is never limited.

trunk-test.ts on a real Asterisk, limits 1 and 2: a 2nd call from 101's phone and a 3rd in total
are refused (with both 0 the same check fails). call-limits.test.ts covers the 0 combinations.
Also fixes the Security tab's hint: browser phones still work from anywhere.

Task: M11-T8
This commit is contained in:
iamdoubz committed 2026-10-20 13:14:20 -05:00
1 parent e4e50f1c0d
commit f6a9b9a4b1
25 files changed
+203 -9

No files matched your search

+33
View File
@@ -52,6 +52,8 @@ function snapshot(): Snapshot {
.map((k) => [k, SettingsSchemas[k].parse(undefined)]),
) as Snapshot['settings'];
settings['routing.defaultCallerIdDidId'] = DID_ID;
settings['calls.maxPerExtension'] = 1;
settings['calls.maxTotal'] = 2;
const trunk = (id: string, name: string, password: string) => ({
id,
name,
@@ -398,6 +400,37 @@ async function main() {
log('class check: ss-noservice');
await ami.command('channel request hangup all');
// Outside calls at once (08 §1), 1 per extension and 2 in total here. Local channels carry
// __SP_LIMITEXT like a forward of that extension's; the second try is 101's own phone.
dials.length = 0;
const out = (num: string, ext: string) =>
ami.action({
Action: 'Originate',
Channel: `Local/${num}@sp-from-ext/n`, // /n: no optimizing away (and its GROUP with it)
Application: 'Wait',
Data: '30',
Variable: `__SP_CLASS=2,__SP_LIMITEXT=${ext}`,
Async: 'true',
});
const dialed = (num: string) =>
dials.some(
(m) =>
String(m.DestChannel).startsWith('PJSIP/trunk-') && String(m.DialString).startsWith(num),
);
await out('13125550001', '101');
await waitFor("101's first outside call", async () => dialed('13125550001'), 15);
await ami.command('channel originate PJSIP/101 extension 13125550002@sp-from-ext');
await out('13125550003', '102');
await waitFor("102's call (2 in total)", async () => dialed('13125550003'), 15);
await out('13125550004', '103');
await sleep(5000);
if (dialed('13125550002')) problems.push('101 had 2 outside calls at once, limit 1');
if (dialed('13125550004')) problems.push('3 outside calls at once, total limit 2');
if (!dialed('13125550002') && !dialed('13125550004'))
log('call limits: a 2nd call for 101 and a 3rd in total were refused');
await ami.command('channel request hangup all');
await sleep(2000);
// M11-T8: SIP allows "," in a dialed number. Unfiltered, Goto(sp-route-…,${EXTEN},1) became
// Goto(sp-route-…,011…,4,1) and started past the class check: international for anyone.
await sleep(1000);