/** * SIP proxy — hub model entry point. * * Thin bootstrap that wires together: * - UDP socket for all SIP signaling * - CallManager (the hub model core) * - Provider registration * - Local device registrar * - WebRTC signaling * - Web dashboard * - Rust codec bridge * * All call/media logic lives in ts/call/. */ import dgram from 'node:dgram'; import fs from 'node:fs'; import path from 'node:path'; import { Buffer } from 'node:buffer'; import { SipMessage } from './sip/index.ts'; import type { IEndpoint } from './sip/index.ts'; import { loadConfig, resolveOutboundRoute } from './config.ts'; import type { IAppConfig, IProviderConfig } from './config.ts'; import { initProviderStates, syncProviderStates, getProviderByUpstreamAddress, handleProviderRegistrationResponse, } from './providerstate.ts'; import { initRegistrar, handleDeviceRegister, isKnownDeviceAddress, getAllDeviceStatuses, } from './registrar.ts'; import { broadcastWs, initWebUi } from './frontend.ts'; import { initWebRtcSignaling, sendToBrowserDevice, getAllBrowserDeviceIds, getBrowserDeviceWs, } from './webrtcbridge.ts'; import { initCodecBridge } from './opusbridge.ts'; import { initAnnouncement } from './announcement.ts'; import { CallManager } from './call/index.ts'; import { PromptCache } from './call/prompt-cache.ts'; import { VoiceboxManager } from './voicebox.ts'; // --------------------------------------------------------------------------- // Config // --------------------------------------------------------------------------- let appConfig: IAppConfig = loadConfig(); const { proxy } = appConfig; const LAN_IP = proxy.lanIp; const LAN_PORT = proxy.lanPort; const LOG_PATH = path.join(process.cwd(), 'sip_trace.log'); // --------------------------------------------------------------------------- // Logging // --------------------------------------------------------------------------- const startTime = Date.now(); const instanceId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; function now(): string { return new Date().toISOString().replace('T', ' ').slice(0, 19); } function log(msg: string): void { const line = `${now()} ${msg}\n`; fs.appendFileSync(LOG_PATH, line); process.stdout.write(line); broadcastWs('log', { message: msg }); } function logPacket(label: string, data: Buffer): void { const head = `\n========== ${now()} ${label} (${data.length}b) ==========\n`; const looksText = data.length > 0 && data[0] >= 0x41 && data[0] <= 0x7a; const body = looksText ? data.toString('utf8') : `[${data.length} bytes binary] ${data.toString('hex').slice(0, 80)}`; fs.appendFileSync(LOG_PATH, head + body + '\n'); } // --------------------------------------------------------------------------- // Initialize subsystems // --------------------------------------------------------------------------- const providerStates = initProviderStates(appConfig.providers, proxy.publicIpSeed); initRegistrar(appConfig.devices, log); // Initialize voicemail and IVR subsystems. const promptCache = new PromptCache(log); const voiceboxManager = new VoiceboxManager(log); voiceboxManager.init(appConfig.voiceboxes ?? []); const callManager = new CallManager({ appConfig, sendSip: (buf, dest) => sock.send(buf, dest.port, dest.address), log, broadcastWs, getProviderState: (id) => providerStates.get(id), getAllBrowserDeviceIds, sendToBrowserDevice, getBrowserDeviceWs, promptCache, voiceboxManager, }); // Initialize WebRTC signaling (browser device registration only). initWebRtcSignaling({ log }); // --------------------------------------------------------------------------- // Status snapshot (fed to web dashboard) // --------------------------------------------------------------------------- function getStatus() { const providers: unknown[] = []; for (const ps of providerStates.values()) { providers.push({ id: ps.config.id, displayName: ps.config.displayName, registered: ps.isRegistered, publicIp: ps.publicIp, }); } return { instanceId, uptime: Math.floor((Date.now() - startTime) / 1000), lanIp: LAN_IP, providers, devices: getAllDeviceStatuses(), calls: callManager.getStatus(), callHistory: callManager.getHistory(), contacts: appConfig.contacts || [], voicemailCounts: voiceboxManager.getAllUnheardCounts(), }; } // --------------------------------------------------------------------------- // Main UDP socket // --------------------------------------------------------------------------- const sock = dgram.createSocket('udp4'); sock.on('message', (data: Buffer, rinfo: dgram.RemoteInfo) => { try { const ps = getProviderByUpstreamAddress(rinfo.address, rinfo.port); const msg = SipMessage.parse(data); if (!msg) { // Non-SIP data — forward raw based on direction. if (ps) { // From provider, forward to... nowhere useful without a call context. logPacket(`UP->DN RAW (unparsed) from ${rinfo.address}:${rinfo.port}`, data); } else { // From device, forward to default provider. const dp = resolveOutboundRoute(appConfig, ''); if (dp) sock.send(data, dp.provider.outboundProxy.port, dp.provider.outboundProxy.address); } return; } // 1. Provider registration responses — consumed by providerstate. if (handleProviderRegistrationResponse(msg)) return; // 2. Device REGISTER — handled by local registrar. if (!ps && msg.method === 'REGISTER') { const response = handleDeviceRegister(msg, { address: rinfo.address, port: rinfo.port }); if (response) { sock.send(response.serialize(), rinfo.port, rinfo.address); return; } } // 3. Route to existing call by SIP Call-ID. if (callManager.routeSipMessage(msg, rinfo)) { return; } // 4. New inbound call from provider. if (ps && msg.isRequest && msg.method === 'INVITE') { logPacket(`[new inbound] INVITE from ${rinfo.address}:${rinfo.port}`, data); // Detect public IP from Via. const via = msg.getHeader('Via'); if (via) ps.detectPublicIp(via); callManager.createInboundCall(ps, msg, { address: rinfo.address, port: rinfo.port }); return; } // 5. New outbound call from device (passthrough). if (!ps && msg.isRequest && msg.method === 'INVITE') { logPacket(`[new outbound] INVITE from ${rinfo.address}:${rinfo.port}`, data); const dialedNumber = SipMessage.extractUri(msg.requestUri || '') || ''; const routeResult = resolveOutboundRoute( appConfig, dialedNumber, undefined, (pid) => !!providerStates.get(pid)?.registeredAor, ); if (routeResult) { const provState = providerStates.get(routeResult.provider.id); if (provState) { // Apply number transformation to the INVITE if needed. if (routeResult.transformedNumber !== dialedNumber) { const newUri = msg.requestUri?.replace(dialedNumber, routeResult.transformedNumber); if (newUri) msg.setRequestUri(newUri); } callManager.handlePassthroughOutbound(msg, { address: rinfo.address, port: rinfo.port }, routeResult.provider, provState); } } return; } // 6. Fallback: forward based on direction (for mid-dialog messages // that don't match any tracked call, e.g. OPTIONS, NOTIFY). if (ps) { // From provider -> forward to device. logPacket(`[fallback inbound] from ${rinfo.address}:${rinfo.port}`, data); const via = msg.getHeader('Via'); if (via) ps.detectPublicIp(via); // Try to figure out where to send it... // For now, just log. These should become rare once all calls are tracked. log(`[fallback] unrouted inbound ${msg.isRequest ? msg.method : msg.statusCode} Call-ID=${msg.callId.slice(0, 30)}`); } else { // From device -> forward to provider. logPacket(`[fallback outbound] from ${rinfo.address}:${rinfo.port}`, data); const fallback = resolveOutboundRoute(appConfig, ''); if (fallback) sock.send(msg.serialize(), fallback.provider.outboundProxy.port, fallback.provider.outboundProxy.address); } } catch (e: any) { log(`[err] ${e?.stack || e}`); } }); sock.on('error', (err: Error) => log(`[main] sock err: ${err.message}`)); sock.bind(LAN_PORT, '0.0.0.0', () => { const providerList = appConfig.providers.map((p) => p.displayName).join(', '); const deviceList = appConfig.devices.map((d) => d.displayName).join(', '); log(`sip proxy bound 0.0.0.0:${LAN_PORT} | providers: ${providerList} | devices: ${deviceList}`); // Start upstream provider registrations. for (const ps of providerStates.values()) { ps.startRegistration( LAN_IP, LAN_PORT, (buf, dest) => sock.send(buf, dest.port, dest.address), log, (provider) => broadcastWs('registration', { providerId: provider.config.id, registered: provider.isRegistered }), ); } // Initialize audio codec bridge (Rust binary via smartrust). initCodecBridge(log) .then(() => initAnnouncement(log)) .then(async () => { // Pre-generate voicemail beep tone. await promptCache.generateBeep('voicemail-beep', 1000, 500, 8000); // Pre-generate voicemail greetings for all configured voiceboxes. for (const vb of appConfig.voiceboxes ?? []) { if (!vb.enabled) continue; const promptId = `voicemail-greeting-${vb.id}`; const wavPath = vb.greetingWavPath; if (wavPath) { await promptCache.loadWavPrompt(promptId, wavPath); } else { const text = vb.greetingText || 'The person you are trying to reach is not available. Please leave a message after the tone.'; await promptCache.generatePrompt(promptId, text, vb.greetingVoice || 'af_bella'); } } // Pre-generate IVR menu prompts. if (appConfig.ivr?.enabled) { for (const menu of appConfig.ivr.menus) { const promptId = `ivr-menu-${menu.id}`; await promptCache.generatePrompt(promptId, menu.promptText, menu.promptVoice || 'af_bella'); } } log(`[startup] prompts cached: ${promptCache.listIds().join(', ') || 'none'}`); }) .catch((e) => log(`[codec] init failed: ${e}`)); }); // --------------------------------------------------------------------------- // Web UI // --------------------------------------------------------------------------- initWebUi( getStatus, log, (number, deviceId, providerId) => { const call = callManager.createOutboundCall(number, deviceId, providerId); return call ? { id: call.id } : null; }, (callId) => callManager.hangup(callId), () => { // Reload config after UI save. try { const fresh = loadConfig(); Object.assign(appConfig, fresh); // Sync provider registrations: add new, remove deleted, re-register changed. syncProviderStates( fresh.providers, proxy.publicIpSeed, LAN_IP, LAN_PORT, (buf, dest) => sock.send(buf, dest.port, dest.address), log, (provider) => broadcastWs('registration', { providerId: provider.config.id, registered: provider.isRegistered }), ); log('[config] reloaded config after save'); } catch (e: any) { log(`[config] reload failed: ${e.message}`); } }, callManager, voiceboxManager, ); process.on('SIGINT', () => { log('SIGINT, exiting'); process.exit(0); }); process.on('SIGTERM', () => { log('SIGTERM, exiting'); process.exit(0); });