feat: activate mod_xml_curl with b2bcall-fs-config (agente.md secao 26)
- apps/freeswitch-config (b2bcall-fs-config): Fastify service implementing the mod_xml_curl HTTP protocol (form-encoded POST -> XML response), containerized, no host port published - reactivated mod_xml_curl in FreeSWITCH, binding restricted to directory|dialplan only (configuration was removed after testing showed it firing several unnecessary HTTP round-trips at boot for module configs we don't need dynamic — matches agente.md's own 'don't put every critical config through XML Curl' guidance) - no extensions/dialplan tables exist yet (next phases), so the service always answers 'not found' for now — this phase only proves the wire protocol works without breaking the static vanilla config fallback - verified end-to-end: user/8888 (nowhere) -> SUBSCRIBER_ABSENT via fs-config; user/1000 (static vanilla extension) -> USER_NOT_REGISTERED, proving FreeSWITCH correctly falls through to static XML when xml_curl says not found - docs/XML_CURL.md, including the not-yet-authenticated endpoint note (fine while it only returns not-found; needs gateway-credentials before serving real directory/dialplan data)
This commit is contained in:
16
TODO.md
16
TODO.md
@@ -42,6 +42,18 @@
|
||||
- [ ] Reconciliação pós-reconexão (calls/agents/queues/registrations/gateways)
|
||||
— não é possível ainda, sem essas tabelas persistidas
|
||||
|
||||
## PHASE 07 — XML Curl (agente.md secao 26)
|
||||
- [x] `apps/freeswitch-config` (b2bcall-fs-config): responde ao protocolo
|
||||
XML Curl do FreeSWITCH (POST form-encoded → XML), containerizado
|
||||
- [x] `mod_xml_curl` reativado, binding restrito a `directory|dialplan`
|
||||
(não `configuration` — evita chamadas HTTP desnecessárias no boot)
|
||||
- [x] Por enquanto sempre "not found" (sem tabela extensions/dialplan ainda);
|
||||
verificado que a config estática vanilla continua funcionando como
|
||||
fallback (`user/8888` → SUBSCRIBER_ABSENT via fs-config,
|
||||
`user/1000` → USER_NOT_REGISTERED via config estática — achou o usuário)
|
||||
- [ ] Sem autenticação HTTP ainda — ok enquanto só responde "not found";
|
||||
adicionar `gateway-credentials` antes de servir directory/dialplan reais
|
||||
|
||||
## PHASE 02 — SaaS Core
|
||||
- [x] Monorepo Node.js/TypeScript (pnpm workspaces, tsconfig base)
|
||||
- [x] Node 22 LTS + pnpm instalados no host
|
||||
@@ -73,8 +85,8 @@
|
||||
— testado ponta a ponta com curl (login, refresh rotation, logout, RBAC, 401/403/429)
|
||||
- [ ] Password reset por e-mail — depende de SMTP configurado
|
||||
|
||||
## PHASE 07+ — ver `agente.md` seções 26 em diante (XML Curl, Extensions, Trunks, Dialplan,
|
||||
Call Center, Predictive Dialer, Recordings, AI, Billing, Frontend, Reports, Security, Tests)
|
||||
## PHASE 08+ — ver `agente.md` seções 39 em diante (Extensions, Trunks, Dialplan, Call Center,
|
||||
Predictive Dialer, Recordings, AI, Billing, Frontend, Reports, Security, Tests)
|
||||
|
||||
---
|
||||
|
||||
|
||||
17
apps/freeswitch-config/Dockerfile
Normal file
17
apps/freeswitch-config/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
FROM node:22-slim
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@11.24.0 --activate
|
||||
|
||||
WORKDIR /repo
|
||||
|
||||
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml tsconfig.base.json ./
|
||||
COPY packages/types packages/types
|
||||
COPY packages/shared packages/shared
|
||||
COPY apps/freeswitch-config apps/freeswitch-config
|
||||
|
||||
RUN pnpm install --frozen-lockfile --filter @b2bcall/freeswitch-config...
|
||||
|
||||
WORKDIR /repo/apps/freeswitch-config
|
||||
|
||||
CMD ["pnpm", "exec", "tsx", "src/main.ts"]
|
||||
21
apps/freeswitch-config/package.json
Normal file
21
apps/freeswitch-config/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@b2bcall/freeswitch-config",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/main.ts",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"start": "node dist/main.js",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@b2bcall/shared": "workspace:*",
|
||||
"@fastify/formbody": "^8.0.1",
|
||||
"fastify": "5.12.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"tsx": "^4.23.12",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
}
|
||||
58
apps/freeswitch-config/src/main.ts
Normal file
58
apps/freeswitch-config/src/main.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import Fastify from "fastify";
|
||||
import formbody from "@fastify/formbody";
|
||||
import { createLogger } from "@b2bcall/shared";
|
||||
|
||||
const logger = createLogger("b2bcall-fs-config");
|
||||
|
||||
/**
|
||||
* Resposta padrão do protocolo XML Curl pra "não achei nada aqui" — o
|
||||
* FreeSWITCH cai de volta pras outras fontes XML registradas (a config
|
||||
* estática vanilla continua funcionando normalmente).
|
||||
*/
|
||||
const NOT_FOUND_XML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="freeswitch/xml">
|
||||
<section name="result">
|
||||
<result status="not found"/>
|
||||
</section>
|
||||
</document>`;
|
||||
|
||||
interface XmlCurlBody {
|
||||
section?: string;
|
||||
tag_name?: string;
|
||||
key_name?: string;
|
||||
key_value?: string;
|
||||
purpose?: string;
|
||||
user?: string;
|
||||
domain?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const app = Fastify({ logger: false });
|
||||
await app.register(formbody);
|
||||
|
||||
app.post<{ Body: XmlCurlBody }>("/", async (request, reply) => {
|
||||
const { section, purpose, user, domain } = request.body ?? {};
|
||||
|
||||
logger.info("requisicao xml_curl recebida", { section, purpose, user, domain });
|
||||
|
||||
// Nenhuma tabela de extensions/dialplan existe ainda (fases Extensions/
|
||||
// Trunks/Dialplan, agente.md secao 232). Por enquanto respondemos
|
||||
// sempre "not found" — prova o encanamento (FreeSWITCH -> mod_xml_curl
|
||||
// -> este servico -> XML valido) sem afetar a config estatica vanilla,
|
||||
// que continua sendo consultada como fallback.
|
||||
reply.header("Content-Type", "text/xml");
|
||||
return NOT_FOUND_XML;
|
||||
});
|
||||
|
||||
app.get("/health", async () => ({ status: "ok" }));
|
||||
|
||||
const port = Number(process.env.PORT ?? 8080);
|
||||
await app.listen({ port, host: "0.0.0.0" });
|
||||
logger.info(`b2bcall-fs-config ouvindo na porta ${port}`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
logger.error("falha fatal ao iniciar b2bcall-fs-config", { error: String(err) });
|
||||
process.exit(1);
|
||||
});
|
||||
9
apps/freeswitch-config/tsconfig.json
Normal file
9
apps/freeswitch-config/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -32,6 +32,20 @@ services:
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
fs-config:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: apps/freeswitch-config/Dockerfile
|
||||
container_name: b2bcall-fs-config
|
||||
restart: unless-stopped
|
||||
# Sem porta publicada: so o FreeSWITCH (mesma rede do compose) chama isto.
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "fetch('http://localhost:8080/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
|
||||
freeswitch:
|
||||
build:
|
||||
context: ./infrastructure/freeswitch
|
||||
@@ -39,6 +53,8 @@ services:
|
||||
- freeswitch_pat
|
||||
container_name: b2bcall-freeswitch
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- fs-config
|
||||
environment:
|
||||
ESL_PASSWORD: ${ESL_PASSWORD}
|
||||
# Nenhuma porta publicada no host: SIP/RTP ainda não têm troncos reais
|
||||
|
||||
57
docs/XML_CURL.md
Normal file
57
docs/XML_CURL.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# XML Curl
|
||||
|
||||
`b2bcall-fs-config` (`apps/freeswitch-config`) responde às consultas HTTP que
|
||||
`mod_xml_curl` faz ao FreeSWITCH pra Directory e Dialplan (agente.md secao 26).
|
||||
|
||||
## Fluxo
|
||||
|
||||
```
|
||||
FreeSWITCH → mod_xml_curl → http://fs-config:8080/ → (nesta fase: sempre "not found")
|
||||
```
|
||||
|
||||
O binding cobre só `directory|dialplan` (não `configuration`) — inicialmente
|
||||
incluí `configuration` também, mas isso disparava várias chamadas HTTP
|
||||
desnecessárias no boot do FreeSWITCH pra configs de módulo que não precisamos
|
||||
controlar dinamicamente (contraria a seção 26: "Não colocar todas as
|
||||
configurações críticas via XML Curl").
|
||||
|
||||
## Por que "not found" pra tudo, por enquanto
|
||||
|
||||
Não existe tabela `extensions` nem `dialplans` ainda — essas são as próximas
|
||||
fases (Extensions, Trunks, Dialplan). O objetivo desta fase era só provar o
|
||||
encanamento: FreeSWITCH consegue mesmo chamar nosso serviço e interpretar a
|
||||
resposta corretamente, **sem quebrar a config estática vanilla que continua
|
||||
valendo como fallback**.
|
||||
|
||||
Isso foi verificado assim:
|
||||
|
||||
```bash
|
||||
# Usuario inexistente em qualquer lugar -> vai ate o fs-config, recebe "not
|
||||
# found", FreeSWITCH reporta erro correto:
|
||||
fs_cli -x "originate user/8888 &park()" # -ERR SUBSCRIBER_ABSENT
|
||||
|
||||
# Usuario 1000 (extensao estatica da config vanilla) -> mod_xml_curl retorna
|
||||
# "not found", FreeSWITCH cai pro XML estatico e ACHA o usuario (so nao tem
|
||||
# telefone registrado, que e' o esperado sem SIP real ainda):
|
||||
fs_cli -x "originate user/1000 &park()" # -ERR USER_NOT_REGISTERED (nao SUBSCRIBER_ABSENT)
|
||||
```
|
||||
|
||||
A diferença entre as duas mensagens de erro é a prova de que o fallback pra
|
||||
config estática funciona corretamente.
|
||||
|
||||
## Segurança
|
||||
|
||||
Sem autenticação no endpoint HTTP por enquanto — a única proteção é isolamento
|
||||
de rede (porta não publicada no host, só o FreeSWITCH na mesma rede do
|
||||
Docker Compose alcança). Isso é aceitável **enquanto o serviço só responde
|
||||
"not found"** (não há nada de valor pra proteger ainda). `mod_xml_curl`
|
||||
suporta `gateway-credentials` (HTTP Basic) nativamente — adicionar isso
|
||||
**antes** deste serviço passar a devolver directory/dialplan reais (fase
|
||||
Extensions/Trunks/Dialplan), já que a partir daí ele vira fonte de verdade
|
||||
para autenticação SIP.
|
||||
|
||||
## Próxima fase (Extensions)
|
||||
|
||||
Quando a tabela `extensions` existir, o handler `section === "directory"`
|
||||
passa a consultar o Postgres e devolver o XML real de usuário (senha SIP,
|
||||
context, etc.) em vez de "not found" fixo.
|
||||
@@ -45,6 +45,7 @@ RUN --mount=type=secret,id=freeswitch_pat,required=true \
|
||||
COPY overrides/autoload_configs/modules.conf.xml /etc/freeswitch/autoload_configs/modules.conf.xml
|
||||
COPY overrides/autoload_configs/event_socket.conf.xml /etc/freeswitch/autoload_configs/event_socket.conf.xml
|
||||
COPY overrides/autoload_configs/acl.conf.xml /etc/freeswitch/autoload_configs/acl.conf.xml
|
||||
COPY overrides/autoload_configs/xml_curl.conf.xml /etc/freeswitch/autoload_configs/xml_curl.conf.xml
|
||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
|
||||
@@ -8,11 +8,7 @@
|
||||
<load module="mod_enum"/>
|
||||
|
||||
<!-- XML Interfaces (agente.md secao 26: mod_xml_curl para Directory/Dialplan) -->
|
||||
<!-- Desativado por enquanto: mod_xml_curl recusa carregar sem pelo menos
|
||||
um binding com gateway-url configurada (falha "Binding has no url!"
|
||||
com o xml_curl.conf.xml default, que vem com tudo comentado). Ativar
|
||||
junto com o b2bcall-fs-config na fase "XML Curl" do agente.md. -->
|
||||
<!-- <load module="mod_xml_curl"/> -->
|
||||
<load module="mod_xml_curl"/>
|
||||
|
||||
<!-- Event Handlers -->
|
||||
<load module="mod_cdr_csv"/>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<configuration name="xml_curl.conf" description="cURL XML Gateway">
|
||||
<bindings>
|
||||
<binding name="b2bcall-fs-config">
|
||||
<!-- b2bcall-fs-config ainda so responde "not found" pra tudo (nao
|
||||
existe extensions/dialplan persistidos ainda — fases seguintes).
|
||||
A config estatica vanilla continua valendo como fallback. -->
|
||||
<param name="gateway-url" value="http://fs-config:8080/" bindings="directory|dialplan"/>
|
||||
<param name="timeout" value="5"/>
|
||||
</binding>
|
||||
</bindings>
|
||||
</configuration>
|
||||
35
pnpm-lock.yaml
generated
35
pnpm-lock.yaml
generated
@@ -70,6 +70,28 @@ importers:
|
||||
specifier: ^5.7.0
|
||||
version: 5.9.3
|
||||
|
||||
apps/freeswitch-config:
|
||||
dependencies:
|
||||
'@b2bcall/shared':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/shared
|
||||
'@fastify/formbody':
|
||||
specifier: ^8.0.1
|
||||
version: 8.0.2
|
||||
fastify:
|
||||
specifier: 5.12.1
|
||||
version: 5.12.1
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.0.0
|
||||
version: 22.20.1
|
||||
tsx:
|
||||
specifier: ^4.23.12
|
||||
version: 4.23.12
|
||||
typescript:
|
||||
specifier: ^5.7.0
|
||||
version: 5.9.3
|
||||
|
||||
apps/freeswitch-events:
|
||||
dependencies:
|
||||
'@b2bcall/shared':
|
||||
@@ -369,6 +391,9 @@ packages:
|
||||
'@fastify/fast-json-stringify-compiler@5.1.0':
|
||||
resolution: {integrity: sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==}
|
||||
|
||||
'@fastify/formbody@8.0.2':
|
||||
resolution: {integrity: sha512-84v5J2KrkXzjgBpYnaNRPqwgMsmY7ZDjuj0YVuMR3NXCJRCgKEZy/taSP1wUYGn0onfxJpLyRGDLa+NMaDJtnA==}
|
||||
|
||||
'@fastify/formbody@9.0.0':
|
||||
resolution: {integrity: sha512-T/af26CSrUARBCvsEmv+DJLPfZlrRKESzqironxP1j7qzuLyKcoZtj6MuTGShuKx1THXugoie2oFbUJxXfGFzA==}
|
||||
|
||||
@@ -963,6 +988,9 @@ packages:
|
||||
fast-uri@4.1.3:
|
||||
resolution: {integrity: sha512-7+72G6vLt7jjNas8SmSATx2qeyRIjxeqO3i4IkmDTxlqYZRKANhOe1bnovcp4WZmvsYrp60WyqPyHqgRiX0yXw==}
|
||||
|
||||
fastify-plugin@5.1.0:
|
||||
resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==}
|
||||
|
||||
fastify-plugin@6.0.0:
|
||||
resolution: {integrity: sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==}
|
||||
|
||||
@@ -1527,6 +1555,11 @@ snapshots:
|
||||
dependencies:
|
||||
fast-json-stringify: 7.0.1
|
||||
|
||||
'@fastify/formbody@8.0.2':
|
||||
dependencies:
|
||||
fast-querystring: 1.1.2
|
||||
fastify-plugin: 5.1.0
|
||||
|
||||
'@fastify/formbody@9.0.0':
|
||||
dependencies:
|
||||
fast-querystring: 1.1.2
|
||||
@@ -2164,6 +2197,8 @@ snapshots:
|
||||
|
||||
fast-uri@4.1.3: {}
|
||||
|
||||
fastify-plugin@5.1.0: {}
|
||||
|
||||
fastify-plugin@6.0.0: {}
|
||||
|
||||
fastify@5.12.1:
|
||||
|
||||
Reference in New Issue
Block a user