asterisk: AMI limited to the classes we use, no shell-running modules in the image

This commit is contained in:
iamdoubz committed 2026-10-29 18:35:06 -05:00
1 parent 6bd2aaec56
commit 63286a1954
12 files changed
+52 -34

No files matched your search

+3 -5
View File
@@ -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);
+2 -4
View File
@@ -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,
+2 -4
View File
@@ -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',
+18
View File
@@ -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;
}
+2 -4
View File
@@ -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;
+2 -4
View File
@@ -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',
+2 -4
View File
@@ -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);
+2
View File
@@ -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
+8 -2
View File
@@ -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
+2 -1
View File
@@ -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
+7 -2
View File
@@ -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
+2 -4
View File
@@ -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(