diff --git a/apps/api/test/asterisk/call-test.ts b/apps/api/test/asterisk/call-test.ts index 312eae6..53246a4 100644 --- a/apps/api/test/asterisk/call-test.ts +++ b/apps/api/test/asterisk/call-test.ts @@ -13,6 +13,7 @@ import { SettingsSchemas, type SettingKey } from '@selfphoneng/shared'; import { AmiClient } from '../../src/ami/client.ts'; import { GENERATED_FILES, render } from '../../src/generator/index.ts'; import type { Snapshot } from '../../src/generator/load.ts'; +import { testManagerConf } from './manager-conf.ts'; const IMAGE = process.env.ASTERISK_IMAGE ?? 'selfphoneng-asterisk'; const CALL_SECONDS = Number(process.env.CALL_SECONDS ?? 30); @@ -126,11 +127,8 @@ async function main() { spawnSync('mkdir', ['-p', conf]); const files = render(snapshot()); for (const f of GENERATED_FILES) writeFileSync(join(conf, f), files[f]); - // Test-only manager.conf: reachable through the published port (the real one is 127.0.0.1-only). - writeFileSync( - join(dir, 'manager.conf.template'), - '[general]\nenabled = yes\nport = 5038\nbindaddr = 0.0.0.0\n\n[selfphoneng]\nsecret = ${AMI_SECRET}\npermit = 0.0.0.0/0.0.0.0\nread = all\nwrite = all\n', - ); + // The real manager.conf, reachable through the published port (test/asterisk/manager-conf.ts). + writeFileSync(join(dir, 'manager.conf.template'), testManagerConf()); phoneConfig(dir, '101', 5070, 440); phoneConfig(dir, '102', 5072, 880); diff --git a/apps/api/test/asterisk/cdr-record.ts b/apps/api/test/asterisk/cdr-record.ts index c697d9d..9254c1f 100644 --- a/apps/api/test/asterisk/cdr-record.ts +++ b/apps/api/test/asterisk/cdr-record.ts @@ -18,6 +18,7 @@ import type { AmiMessage } from '../../src/ami/events.ts'; import { GENERATED_FILES, render } from '../../src/generator/index.ts'; import type { Snapshot, SnapExtension } from '../../src/generator/load.ts'; import { providerDialplan, providerPjsip } from './provider.ts'; +import { testManagerConf } from './manager-conf.ts'; const IMAGE = process.env.ASTERISK_IMAGE ?? 'selfphoneng-asterisk'; const OUT = fileURLToPath(new URL('../fixtures/cdr/', import.meta.url)); @@ -243,10 +244,7 @@ async function main() { ); } const runPhones = phoneFiles(dir); - writeFileSync( - join(dir, 'manager.conf.template'), - '[general]\nenabled = yes\nport = 5038\nbindaddr = 0.0.0.0\n\n[selfphoneng]\nsecret = ${AMI_SECRET}\npermit = 0.0.0.0/0.0.0.0\nread = all\nwrite = all\n', - ); + writeFileSync(join(dir, 'manager.conf.template'), testManagerConf()); const ami = new AmiClient({ host: '127.0.0.1', port: AMI_PORT, diff --git a/apps/api/test/asterisk/cnam-test.ts b/apps/api/test/asterisk/cnam-test.ts index af2600d..4fd4978 100644 --- a/apps/api/test/asterisk/cnam-test.ts +++ b/apps/api/test/asterisk/cnam-test.ts @@ -17,6 +17,7 @@ import { AmiClient } from '../../src/ami/client.ts'; import { GENERATED_FILES, render } from '../../src/generator/index.ts'; import type { Snapshot } from '../../src/generator/load.ts'; import { providerPjsip } from './provider.ts'; +import { testManagerConf } from './manager-conf.ts'; const IMAGE = process.env.ASTERISK_IMAGE ?? 'selfphoneng-asterisk'; const NET = 'sp-cnam'; @@ -223,10 +224,7 @@ async function main() { ); } phoneFiles(dir); - writeFileSync( - join(dir, 'manager.conf.template'), - '[general]\nenabled = yes\nport = 5038\nbindaddr = 0.0.0.0\n\n[selfphoneng]\nsecret = ${AMI_SECRET}\npermit = 0.0.0.0/0.0.0.0\nread = all\nwrite = all\n', - ); + writeFileSync(join(dir, 'manager.conf.template'), testManagerConf()); const problems: string[] = []; const ami = new AmiClient({ host: '127.0.0.1', diff --git a/apps/api/test/asterisk/manager-conf.ts b/apps/api/test/asterisk/manager-conf.ts new file mode 100644 index 0000000..a3ae73e --- /dev/null +++ b/apps/api/test/asterisk/manager-conf.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// The real manager.conf.template, read/write classes included, so a class the api needs and the +// template lacks fails the tests. Only opened up for the host to reach AMI through the published +// port: the real one is 127.0.0.1-only. +import { readFileSync } from 'node:fs'; + +export function testManagerConf(): string { + const real = readFileSync( + new URL('../../../../asterisk/conf/manager.conf.template', import.meta.url), + 'utf8', + ); + const open = real + .replace(/^bindaddr = 127\.0\.0\.1$/m, 'bindaddr = 0.0.0.0') + .replace(/^permit = 127\.0\.0\.1\/255\.255\.255\.255$/m, 'permit = 0.0.0.0/0.0.0.0'); + if (open.includes('127.0.0.1')) + throw new Error('manager.conf.template changed: update manager-conf.ts'); + return open; +} diff --git a/apps/api/test/asterisk/trunk-test.ts b/apps/api/test/asterisk/trunk-test.ts index 036d190..4bbf2b8 100644 --- a/apps/api/test/asterisk/trunk-test.ts +++ b/apps/api/test/asterisk/trunk-test.ts @@ -17,6 +17,7 @@ import { GENERATED_FILES, render } from '../../src/generator/index.ts'; import type { Snapshot } from '../../src/generator/load.ts'; import { providerDialplan, providerPjsip } from './provider.ts'; +import { testManagerConf } from './manager-conf.ts'; const IMAGE = process.env.ASTERISK_IMAGE ?? 'selfphoneng-asterisk'; const RECORD = process.argv.includes('--record') @@ -200,10 +201,7 @@ async function main() { ); } phoneFiles(dir); - writeFileSync( - join(dir, 'manager.conf.template'), - '[general]\nenabled = yes\nport = 5038\nbindaddr = 0.0.0.0\n\n[selfphoneng]\nsecret = ${AMI_SECRET}\npermit = 0.0.0.0/0.0.0.0\nread = all\nwrite = all\n', - ); + writeFileSync(join(dir, 'manager.conf.template'), testManagerConf()); const problems: string[] = []; let recorder: Socket | undefined; diff --git a/apps/api/test/asterisk/upgrade-test.ts b/apps/api/test/asterisk/upgrade-test.ts index ebfbe6d..77e9249 100644 --- a/apps/api/test/asterisk/upgrade-test.ts +++ b/apps/api/test/asterisk/upgrade-test.ts @@ -13,6 +13,7 @@ import { fileURLToPath } from 'node:url'; import { AmiClient } from '../../src/ami/client.ts'; import { field, type AmiMessage } from '../../src/ami/events.ts'; import { FACTS, VERIFIED_ASTERISK, pinnedAsterisk, type Fact } from './verified.ts'; +import { testManagerConf } from './manager-conf.ts'; const IMAGE = process.env.ASTERISK_IMAGE ?? 'selfphoneng-asterisk'; const GOLDEN = fileURLToPath( @@ -146,10 +147,7 @@ async function main() { appendFileSync(join(pbxConf, 'extensions_selfphoneng.conf'), pbxDialplan); for (const f of readdirSync(GOLDEN)) writeFileSync(join(provConf, f), f === 'pjsip_selfphoneng.conf' ? providerPjsip : ''); - writeFileSync( - join(pbxConf, 'manager.conf.template'), - '[general]\nenabled = yes\nport = 5038\nbindaddr = 0.0.0.0\n\n[selfphoneng]\nsecret = ${AMI_SECRET}\npermit = 0.0.0.0/0.0.0.0\nread = all\nwrite = all\n', - ); + writeFileSync(join(pbxConf, 'manager.conf.template'), testManagerConf()); const ami = new AmiClient({ host: '127.0.0.1', diff --git a/apps/api/test/asterisk/voicemail-test.ts b/apps/api/test/asterisk/voicemail-test.ts index 801e4c2..6ca3ed8 100644 --- a/apps/api/test/asterisk/voicemail-test.ts +++ b/apps/api/test/asterisk/voicemail-test.ts @@ -11,6 +11,7 @@ import { SettingsSchemas, type SettingKey } from '@selfphoneng/shared'; import { AmiClient } from '../../src/ami/client.ts'; import { GENERATED_FILES, render } from '../../src/generator/index.ts'; import type { Snapshot } from '../../src/generator/load.ts'; +import { testManagerConf } from './manager-conf.ts'; const IMAGE = process.env.ASTERISK_IMAGE ?? 'selfphoneng-asterisk'; const NET = 'sp-vm'; @@ -111,10 +112,7 @@ async function main() { spawnSync('mkdir', ['-p', conf]); const files = render(snapshot()); for (const f of GENERATED_FILES) writeFileSync(join(conf, f), files[f]); - writeFileSync( - join(dir, 'manager.conf.template'), - '[general]\nenabled = yes\nport = 5038\nbindaddr = 0.0.0.0\n\n[selfphoneng]\nsecret = ${AMI_SECRET}\npermit = 0.0.0.0/0.0.0.0\nread = all\nwrite = all\n', - ); + writeFileSync(join(dir, 'manager.conf.template'), testManagerConf()); phoneConfig(dir, '101', 5070); phoneConfig(dir, '102', 5072); diff --git a/asterisk/Dockerfile b/asterisk/Dockerfile index f02ce84..a76137d 100644 --- a/asterisk/Dockerfile +++ b/asterisk/Dockerfile @@ -28,7 +28,9 @@ RUN ./configure --with-pjproject-bundled --with-jansson-bundled \ && mkdir -p /out/usr/share/selfphoneng \ && mv /out/var/lib/asterisk/moh /out/usr/share/selfphoneng/moh-opsound # Debian's /var/run is a symlink to /run; a real directory here breaks the COPY below. +# Modules that run host commands go: AMI's ModuleLoad (class system) loads them past noload (08 §8). RUN rm -rf /out/var/run \ + && for m in app_system func_shell app_disa res_agi app_externalivr; do rm /out/usr/lib/asterisk/modules/$m.so; done \ && mkdir -p /out/etc/asterisk \ && cp configs/samples/indications.conf.sample /out/etc/asterisk/indications.conf diff --git a/asterisk/conf/manager.conf.template b/asterisk/conf/manager.conf.template index a356610..fe15ce7 100644 --- a/asterisk/conf/manager.conf.template +++ b/asterisk/conf/manager.conf.template @@ -9,6 +9,12 @@ timestampevents = yes secret = ${AMI_SECRET} deny = 0.0.0.0/0.0.0.0 permit = 127.0.0.1/255.255.255.255 -read = all -write = all +; Only the classes the api uses (08 §8). An action needs any one of its classes, so `system` +; still allows GetConfig and ModuleLoad; config (UpdateConfig, CreateConfig) is what this drops. +; Events: system (ContactStatus, Registry), call (channels, bridges, parking, ExtensionStatus, +; MessageWaiting), agent (queues), user (UserEvent, VoicemailPasswordChange), cdr, dialplan (VarSet, Newexten). +read = system,call,agent,user,cdr,dialplan +; Actions: system (DB*, PJSIP*), call (Redirect, Hangup, ParkedCalls), command, originate, +; agent (QueuePause), user (VoicemailMove, VoicemailRemove). +write = system,call,command,originate,agent,user writetimeout = 5000 diff --git a/asterisk/conf/modules.conf b/asterisk/conf/modules.conf index 7974dc8..2b02962 100644 --- a/asterisk/conf/modules.conf +++ b/asterisk/conf/modules.conf @@ -51,7 +51,8 @@ noload = chan_websocket.so noload = res_adsi.so noload = app_getcpeid.so noload = app_adsiprog.so -; can run host commands; nothing of ours uses them, but a dialplan injection would reach for them (08 §6) +; can run host commands; nothing of ours uses them, but a dialplan injection would reach for them (08 §6). +; Our image doesn't have them at all (Dockerfile, 08 §8); these lines cover a custom build. noload = app_system.so noload = func_shell.so noload = app_disa.so diff --git a/docs/spec/01-infrastructure.md b/docs/spec/01-infrastructure.md index 48a38e2..3d06937 100644 --- a/docs/spec/01-infrastructure.md +++ b/docs/spec/01-infrastructure.md @@ -192,7 +192,9 @@ RUN ./configure --with-pjproject-bundled --with-jansson-bundled \ && mkdir -p /out/usr/share/selfphoneng \ && mv /out/var/lib/asterisk/moh /out/usr/share/selfphoneng/moh-opsound # Debian's /var/run is a symlink to /run; a real directory here breaks the COPY below. +# Modules that run host commands go: AMI's ModuleLoad (class system) loads them past noload (08 §8). RUN rm -rf /out/var/run \ + && for m in app_system func_shell app_disa res_agi app_externalivr; do rm /out/usr/lib/asterisk/modules/$m.so; done \ && mkdir -p /out/etc/asterisk \ && cp configs/samples/indications.conf.sample /out/etc/asterisk/indications.conf @@ -369,10 +371,13 @@ timestampevents = yes secret = ${AMI_SECRET} deny = 0.0.0.0/0.0.0.0 permit = 127.0.0.1/255.255.255.255 -read = all -write = all +read = system,call,agent,user,cdr,dialplan +write = system,call,command,originate,agent,user writetimeout = 5000 ``` +Only the classes the api uses (owner, 2026-10-29; the list per action and event is in 08 §8). +The Asterisk tests mount this template with only `bindaddr` and `permit` opened up +(`test/asterisk/manager-conf.ts`), so a missing class fails them. ### 3.5 `http.conf` (WebSocket for the browser softphone) ```ini diff --git a/scripts/e2e/m2-live.mjs b/scripts/e2e/m2-live.mjs index 2d7ab41..966bdce 100644 --- a/scripts/e2e/m2-live.mjs +++ b/scripts/e2e/m2-live.mjs @@ -15,6 +15,7 @@ import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { launch } from './cdp.mjs'; +import { testManagerConf } from '../../apps/api/test/asterisk/manager-conf.ts'; const NET = 'sp-live'; const AST = 'sp-live-ast'; @@ -33,10 +34,7 @@ if (cmd === 'up') { mkdirSync(conf, { recursive: true }); for (const d of [vm, sounds, recs, moh]) if (d) mkdirSync(d, { recursive: true }); const dir = mkdtempSync(join(tmpdir(), 'sp-live-')); - writeFileSync( - join(dir, 'manager.conf.template'), - '[general]\nenabled = yes\nport = 5038\nbindaddr = 0.0.0.0\n\n[selfphoneng]\nsecret = ${AMI_SECRET}\npermit = 0.0.0.0/0.0.0.0\nread = all\nwrite = all\n', - ); + writeFileSync(join(dir, 'manager.conf.template'), testManagerConf()); // The browser softphone (m5-*) runs on the host: SIP-over-WebSocket and a small RTP range are // published on 127.0.0.1, and the api maps the container's ICE candidate to 127.0.0.1. writeFileSync(