import type { HttpContext } from '@adonisjs/core/http' import { buildSoapEnvelope } from '../protocol/soap.js' import { CWMPMessageBuilder } from '../protocol/common/cwmp_message_builder.js' import { StateMachineManager } from '../services/cwmp/state_machine_manager.js' import { Device } from '../domain/device/device.js' import { DeviceId } from '../domain/device/device_id.js' import { DeviceParameter, DeviceProperty } from '../domain/device/device_parameter.js' import DeviceModel from '../models/device.js' import { getDeviceHashIdentifier } from '../utils/device_utils.js' import logger from '@adonisjs/core/services/logger' export default class CwmpController { private static managerInitialized = false private getManager(): StateMachineManager { const manager = StateMachineManager.getInstance() if (!CwmpController.managerInitialized) { manager.startCleanupTimer(60000) CwmpController.managerInitialized = true } return manager } /** * POST /cwmp10 * * Main CWMP/TR-069 endpoint. The cwmp_middleware has already: * - Parsed the raw XML into a SOAP envelope * - Extracted the CWMP message type and data * - Validated content-type * - Attached everything to request.cwmp */ async handle({ request, response }: HttpContext) { const manager = this.getManager() const { sessionId, remoteKey, envelope, message, isEmpty } = request.cwmp try { // Empty body = CPE polling, continue the state machine if (isEmpty) { manager.ensureSessionForRemote(remoteKey, sessionId) if (manager.hasSession(sessionId)) { const result = await manager.continue(sessionId) if (result && result !== '') { return this.sendSoap(response, result) } } return response.status(204).send('') } // Handle Inform if (message?.type === 'Inform') { const result = await this.handleInform(message.data, sessionId, envelope?.header?.id, remoteKey, manager) return this.sendSoap(response, result) } // Handle TransferComplete if (message?.type === 'TransferComplete') { const resp = buildSoapEnvelope(undefined, '') return this.sendSoap(response, resp) } // Other CWMP responses - route to state machine manager.ensureSessionForRemote(remoteKey, sessionId) if (message && manager.hasSession(sessionId)) { const { success, payload } = this.getMessageInfo(message) const result = await manager.handleCWMPResponse(sessionId, success, payload) if (result && result !== '') { return this.sendSoap(response, result) } } return response.status(204).send('') } catch (error: any) { // Return SOAP Fault per TR-069 spec (always HTTP 200) logger.error({ error: error?.message, stack: error?.stack }, 'CWMP error handling request') const faultXml = this.buildSoapFault(error?.message || 'Internal error') return this.sendSoap(response, faultXml) } } // ─── Inform Handling ─────────────────────────────────────────────────────── private async handleInform( informData: any, session: string, cwmpId: string | undefined, remoteKey: string, manager: StateMachineManager ): Promise { logger.info({ informKeys: Object.keys(informData || {}), deviceId: informData?.DeviceId }, 'CWMP handleInform called') const deviceIdStruct = informData?.DeviceId || {} const deviceId = DeviceId.fromInform(deviceIdStruct) // Load or create device let device = await this.loadDevice(deviceId) if (!device) { device = new Device(deviceId) } device.updateLastSeen() // Process inform parameter values const paramValues = informData?.ParameterList?.ParameterValueStruct || [] if (paramValues.length > 0) { device.updateFromValues(paramValues) } device.setTmpParamValues(paramValues) // Persist device const hashId = getDeviceHashIdentifier({ manufacturer: deviceId.manufacturer, oui: deviceId.oui, productClass: deviceId.productClass, serialNumber: deviceId.serialNumber, }) await DeviceModel.createOrUpdate({ identifier: hashId, deviceId, properties: device.getProperties().map((p) => p.toSnapshot()), parameters: device.getParameters().map((p) => p.toSnapshot()), gpnStatus: device.gpnStatus, updatedAt: new Date(), }) // Extract events const events: string[] = Array.isArray(informData?.Event?.EventStruct) ? informData.Event.EventStruct.map((e: any) => e?.EventCode).filter((c: any): c is string => !!c) : [] logger.info({ events }, 'CWMP Inform events extracted') // Start state machine await manager.startStateMachine(device, session, { events }) manager.bindRemoteToDevice(remoteKey, device.deviceId) // After Inform for onboarding events, enqueue critical reads const onlyConnReq = events.length > 0 && events.every((e) => e === '6 CONNECTION REQUEST') if (!onlyConnReq) { try { const criticalPaths = [ 'InternetGatewayDevice.DeviceInfo.SpecVersion', 'InternetGatewayDevice.DeviceInfo.HardwareVersion', 'InternetGatewayDevice.DeviceInfo.SoftwareVersion', 'InternetGatewayDevice.ManagementServer.ConnectionRequestURL', ] await manager.enqueueImmediate(device, 'get_parameter_values', criticalPaths) } catch (_e) { // Non-critical } } return CWMPMessageBuilder.informResponse(cwmpId) } // ─── Helpers ─────────────────────────────────────────────────────────────── private sendSoap(response: any, xml: string) { response.header('Content-Type', 'text/xml; charset=utf-8') response.header('SOAPAction', '""') return response.send(xml) } private getMessageInfo(message: { type: string; data: any }): { success: boolean; payload: any } { const failTypes = ['Fault'] return { success: !failTypes.includes(message.type), payload: message.data, } } private buildSoapFault(reason: string): string { const body = ` Server ${reason} 8002 ${reason} ` return buildSoapEnvelope(undefined, body) } private async loadDevice(deviceId: DeviceId): Promise { try { const record = await DeviceModel.findByDeviceId(deviceId) if (!record) return null const device = new Device( new DeviceId(record.deviceId.manufacturer, record.deviceId.oui, record.deviceId.productClass, record.deviceId.serialNumber), record.createdAt ) record.properties?.forEach((p: any) => device.setProperty(new DeviceProperty(p.name, p.value, p.type))) record.parameters?.forEach((p: any) => device.setParameter(new DeviceParameter(p.name, p.value, p.type))) device.setGpnStatus(record.gpnStatus ?? 'unknown') return device } catch (_error) { return null } } }