feat: restore strong typing on TR-069/CWMP protocol layer

- Copy cwmp-1-0..4.ts source classes from cwmp-connector (papaye) into
  app/protocol/ as local files (ts-nocheck, external dependency)
- Add soap_envelope.ts: SoapEnvelope, SoapEnvelopeHeader, CwmpID, NAMESPACES
- Add tr069_types.ts: re-exports all CWMP classes + CwmpMessage discriminated
  union + typed task payload interfaces (CWMPTaskType, CWMPTaskPayload, etc.)
- Rewrite cwmp_message_builder.ts to use class-based builders instead of raw
  XML template strings; add transferCompleteResponse() and soapFault()
- soap.ts: extractCwmpMessage returns CwmpMessage union, adds runtime allowlist
  guard against unknown CWMP method names from CPE
- cwmp_middleware.ts: envelope typed SoapEnvelope|null, message CwmpMessage|null
- cwmp_controller.ts: handleInform(informData: Inform), remove any, migrate
  TransferComplete and fault to new builder path, drop buildSoapEnvelope import
- task_queue.ts: CWMPTask.type → CWMPTaskType literal union, payload →
  CWMPTaskPayload, result → CWMPTaskResultData
- device_action_executor.ts: GPV/GPN/GPA responses typed, no more any
- main_state_machine.ts: DeviceContext.message.data → CWMPTaskResultData,
  organizeParameters receives ParameterInfoStruct[] directly
- device.ts: tmpParamValues typed ParameterValueStruct[], organizeParameters
  signature updated, updateFromValues typed
- device_id.ts: fromInform typed with DeviceIdStruct
- soap_envelope.ts: remove unconditional <cwmp:noMoreRequests> header
  (CPE→ACS only per TR-069 §3.7.1.4)
This commit is contained in:
Kevin L'Hereec committed 2026-07-20 15:25:26 +02:00
1 parent 8969355688
commit f3eb8e1650
19 files changed
+2745 -384

No files matched your search

+51 -56
View File
@@ -2,10 +2,21 @@ import { DeviceId } from '../../domain/device/device_id.js'
import { EventEmitter } from 'node:events'
import { CWMPMessageBuilder } from '../../protocol/common/cwmp_message_builder.js'
import logger from '@adonisjs/core/services/logger'
import type {
CWMPTaskType,
CWMPTaskPayload,
CWMPTaskResultData,
SetParameterValuesTaskPayload,
SetParameterAttributesTaskPayload,
ScheduleInformTaskPayload,
DownloadTaskPayload,
UploadTaskPayload,
RebootTaskPayload,
} from '../../protocol/tr069_types.js'
export class TaskResult {
status!: ResultStatus
data: any
data: CWMPTaskResultData = {} as CWMPTaskResultData
timestamp!: Date
}
@@ -17,12 +28,12 @@ export enum ResultStatus {
export interface CWMPTask {
id: string
deviceId: DeviceId
type: string
payload: any
type: CWMPTaskType
payload: CWMPTaskPayload
status: TaskStatus
createdAt: Date
completedAt?: Date
result?: any
result?: CWMPTaskResultData
error?: string
}
@@ -38,53 +49,49 @@ export enum TaskStatus {
* Maps tasks to CWMP message builders and manages callbacks
*/
export class CWMPTaskQueue extends EventEmitter {
static task_map: Record<string, (props: any) => string> = {
get_parameter_names: (parameterPath: string) => {
// Use NextLevel=1 for root paths (e.g. "Device." or "InternetGatewayDevice.")
// to avoid fetching the entire parameter tree in a single response.
// For deeper paths (e.g. "Device.DeviceInfo.") use NextLevel=0 to get all leaves.
const segments = parameterPath.replace(/\.$/, '').split('.')
static task_map: Record<CWMPTaskType, (props: CWMPTaskPayload) => string> = {
get_parameter_names: (parameterPath) => {
const path = parameterPath as string
const segments = path.replace(/\.$/, '').split('.')
const nextLevel = segments.length <= 1
return CWMPMessageBuilder.getParameterNames(undefined, parameterPath, nextLevel)
return CWMPMessageBuilder.getParameterNames(undefined, path, nextLevel)
},
get_parameter_attributes: (parameterNames: string[]) => {
return CWMPMessageBuilder.getParameterAttributes(undefined, parameterNames)
get_parameter_attributes: (parameterNames) => {
return CWMPMessageBuilder.getParameterAttributes(undefined, parameterNames as string[])
},
get_parameter_values: (parameterNames: string[]) => {
return CWMPMessageBuilder.getParameterValues(undefined, parameterNames)
get_parameter_values: (parameterNames) => {
return CWMPMessageBuilder.getParameterValues(undefined, parameterNames as string[])
},
set_parameter_values: (props: { parameterValueList: any[]; parameterKey?: string }) => {
const params = (props.parameterValueList || []).map((p: any) => ({
name: p.Name,
value: String(p.Value?._ ?? ''),
type: p.Value?.$?.Type || 'xsd:string',
set_parameter_values: (props) => {
const p = props as SetParameterValuesTaskPayload
const params = (p.parameterValueList || []).map((pv) => ({
name: pv.Name,
value: String(pv.Value?._ ?? ''),
type: pv.Value?.$?.Type || 'xsd:string',
}))
return CWMPMessageBuilder.setParameterValues(undefined, params)
},
set_parameter_attributes: (attrs: any[]) => {
const mapped = attrs.map((a: any) => ({
name: a.name || a.Name,
notificationChange: !!(a.notificationChange ?? a.NotificationChange),
notification: a.notification ?? a.Notification ?? 0,
accessListChange: !!(a.accessListChange ?? a.AccessListChange),
accessList: a.accessList ?? a.AccessList ?? [],
}))
return CWMPMessageBuilder.setParameterAttributes(undefined, mapped)
set_parameter_attributes: (attrs) => {
return CWMPMessageBuilder.setParameterAttributes(
undefined,
attrs as SetParameterAttributesTaskPayload[]
)
},
schedule_inform: (props: { delayInSeconds: number; commandKey?: string }) => {
return CWMPMessageBuilder.scheduleInform(undefined, props.delayInSeconds || 300, props.commandKey)
schedule_inform: (props) => {
const p = props as ScheduleInformTaskPayload
return CWMPMessageBuilder.scheduleInform(undefined, p.delayInSeconds || 300, p.commandKey)
},
download: (props: any) => {
return CWMPMessageBuilder.download(undefined, props)
download: (props) => {
return CWMPMessageBuilder.download(undefined, props as DownloadTaskPayload)
},
upload: (props: any) => {
return CWMPMessageBuilder.upload(undefined, props)
upload: (props) => {
return CWMPMessageBuilder.upload(undefined, props as UploadTaskPayload)
},
factory_reset: () => {
return CWMPMessageBuilder.factoryReset(undefined)
},
reboot: (props: any) => {
return CWMPMessageBuilder.reboot(undefined, props?.commandKey)
reboot: (props) => {
return CWMPMessageBuilder.reboot(undefined, (props as RebootTaskPayload)?.commandKey)
},
}
@@ -100,8 +107,8 @@ export class CWMPTaskQueue extends EventEmitter {
*/
addTask(
deviceId: DeviceId,
taskType: string,
props: any,
taskType: CWMPTaskType,
props: CWMPTaskPayload,
callback: (result: TaskResult) => Promise<string>
): { xml: string; taskId: string } {
const cwmpTask = this.addCWMPTask(deviceId, taskType, props)
@@ -115,10 +122,8 @@ export class CWMPTaskQueue extends EventEmitter {
cwmpTask.status = TaskStatus.SENT
}
// Track the most recent task created for this device so callers can retrieve its ID
this.lastCreatedTaskIdByDevice.set(JSON.stringify(deviceId), cwmpTask.id)
// Generate CWMP envelope
const builder = CWMPTaskQueue.task_map[taskType]
if (typeof builder !== 'function') {
logger.warn(`No CWMP builder found for task '${taskType}'`)
@@ -128,7 +133,7 @@ export class CWMPTaskQueue extends EventEmitter {
}
/**
* Registers a callback for a task already in the queue (e.g. added via addCWMPTask)
* Registers a callback for a task already in the queue
*/
registerCallback(taskId: string, callback: (result: TaskResult) => Promise<string>): void {
this.taskCallbacks.set(taskId, callback)
@@ -137,7 +142,7 @@ export class CWMPTaskQueue extends EventEmitter {
/**
* Adds a CWMP task (internal)
*/
addCWMPTask(deviceId: DeviceId, type: string, payload: any): CWMPTask {
addCWMPTask(deviceId: DeviceId, type: CWMPTaskType, payload: CWMPTaskPayload): CWMPTask {
const taskId = this.generateTaskId(deviceId, type)
const task: CWMPTask = {
id: taskId,
@@ -163,7 +168,7 @@ export class CWMPTaskQueue extends EventEmitter {
/**
* Retrieve and mark a pending task as SENT for a device
*/
getTaskForDevice(deviceId: DeviceId): { id: string; task: string; props: any } | undefined {
getTaskForDevice(deviceId: DeviceId): { id: string; task: CWMPTaskType; props: CWMPTaskPayload } | undefined {
const deviceKey = JSON.stringify(deviceId)
const queue = this.deviceQueues.get(deviceKey)
if (!queue || queue.length === 0) return undefined
@@ -172,7 +177,6 @@ export class CWMPTaskQueue extends EventEmitter {
const cwmpTask = this.tasks.get(taskId)
if (!cwmpTask) return undefined
// Dequeue
queue.shift()
if (cwmpTask.status === TaskStatus.PENDING) {
@@ -182,7 +186,7 @@ export class CWMPTaskQueue extends EventEmitter {
return { id: taskId, task: cwmpTask.type, props: cwmpTask.payload }
}
completeTask(taskId: string, result: any): void {
completeTask(taskId: string, result: CWMPTaskResultData): void {
const task = this.tasks.get(taskId)
if (!task) return
@@ -206,7 +210,7 @@ export class CWMPTaskQueue extends EventEmitter {
this.emit('taskFailed', task)
}
async executeTaskCallback(taskId: string, status: ResultStatus, data: any): Promise<string> {
async executeTaskCallback(taskId: string, status: ResultStatus, data: CWMPTaskResultData): Promise<string> {
const callback = this.taskCallbacks.get(taskId)
if (callback) {
const result = new TaskResult()
@@ -227,11 +231,6 @@ export class CWMPTaskQueue extends EventEmitter {
return ''
}
/**
* Returns the ID of the most recently created task for a device (via addTask).
* Used by the state machine manager to correlate responses with in-flight tasks
* that were created directly by state handlers (not served from the external queue).
*/
getLastCreatedTaskId(deviceId: DeviceId): string | undefined {
return this.lastCreatedTaskIdByDevice.get(JSON.stringify(deviceId))
}
@@ -239,10 +238,6 @@ export class CWMPTaskQueue extends EventEmitter {
clearPendingTasksForDevice(deviceId: DeviceId): void {
const deviceKey = JSON.stringify(deviceId)
const queue = this.deviceQueues.get(deviceKey) || []
// Clear ALL tasks (PENDING and SENT) — after session completion, no task
// from the previous session should bleed into the next one. Tasks that were
// already SENT (created by state-machine states via addTask()) are ghost
// entries that would otherwise be served to the next session.
for (const taskId of queue) {
this.tasks.delete(taskId)
this.taskCallbacks.delete(taskId)