- 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)
262 lines
7.9 KiB
TypeScript
262 lines
7.9 KiB
TypeScript
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: CWMPTaskResultData = {} as CWMPTaskResultData
|
|
timestamp!: Date
|
|
}
|
|
|
|
export enum ResultStatus {
|
|
Success = 'success',
|
|
Error = 'error',
|
|
}
|
|
|
|
export interface CWMPTask {
|
|
id: string
|
|
deviceId: DeviceId
|
|
type: CWMPTaskType
|
|
payload: CWMPTaskPayload
|
|
status: TaskStatus
|
|
createdAt: Date
|
|
completedAt?: Date
|
|
result?: CWMPTaskResultData
|
|
error?: string
|
|
}
|
|
|
|
export enum TaskStatus {
|
|
PENDING = 'pending',
|
|
SENT = 'sent',
|
|
COMPLETED = 'completed',
|
|
FAILED = 'failed',
|
|
}
|
|
|
|
/**
|
|
* CWMP Task Queue with event-based lifecycle
|
|
* Maps tasks to CWMP message builders and manages callbacks
|
|
*/
|
|
export class CWMPTaskQueue extends EventEmitter {
|
|
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, path, nextLevel)
|
|
},
|
|
get_parameter_attributes: (parameterNames) => {
|
|
return CWMPMessageBuilder.getParameterAttributes(undefined, parameterNames as string[])
|
|
},
|
|
get_parameter_values: (parameterNames) => {
|
|
return CWMPMessageBuilder.getParameterValues(undefined, parameterNames as 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) => {
|
|
return CWMPMessageBuilder.setParameterAttributes(
|
|
undefined,
|
|
attrs as SetParameterAttributesTaskPayload[]
|
|
)
|
|
},
|
|
schedule_inform: (props) => {
|
|
const p = props as ScheduleInformTaskPayload
|
|
return CWMPMessageBuilder.scheduleInform(undefined, p.delayInSeconds || 300, p.commandKey)
|
|
},
|
|
download: (props) => {
|
|
return CWMPMessageBuilder.download(undefined, props as DownloadTaskPayload)
|
|
},
|
|
upload: (props) => {
|
|
return CWMPMessageBuilder.upload(undefined, props as UploadTaskPayload)
|
|
},
|
|
factory_reset: () => {
|
|
return CWMPMessageBuilder.factoryReset(undefined)
|
|
},
|
|
reboot: (props) => {
|
|
return CWMPMessageBuilder.reboot(undefined, (props as RebootTaskPayload)?.commandKey)
|
|
},
|
|
}
|
|
|
|
private tasks: Map<string, CWMPTask> = new Map()
|
|
private deviceQueues: Map<string, string[]> = new Map()
|
|
private taskCallbacks: Map<string, (result: TaskResult) => Promise<string>> = new Map()
|
|
/** Tracks the most recently created task per device (set by addTask) */
|
|
private lastCreatedTaskIdByDevice: Map<string, string> = new Map()
|
|
|
|
/**
|
|
* Adds a task with callback (legacy-compatible API used by DeviceActionExecutor)
|
|
* Returns the CWMP XML envelope and task ID
|
|
*/
|
|
addTask(
|
|
deviceId: DeviceId,
|
|
taskType: CWMPTaskType,
|
|
props: CWMPTaskPayload,
|
|
callback: (result: TaskResult) => Promise<string>
|
|
): { xml: string; taskId: string } {
|
|
const cwmpTask = this.addCWMPTask(deviceId, taskType, props)
|
|
|
|
if (callback) {
|
|
this.taskCallbacks.set(cwmpTask.id, callback)
|
|
}
|
|
|
|
// Mark as SENT immediately
|
|
if (cwmpTask.status === TaskStatus.PENDING) {
|
|
cwmpTask.status = TaskStatus.SENT
|
|
}
|
|
|
|
this.lastCreatedTaskIdByDevice.set(JSON.stringify(deviceId), cwmpTask.id)
|
|
|
|
const builder = CWMPTaskQueue.task_map[taskType]
|
|
if (typeof builder !== 'function') {
|
|
logger.warn(`No CWMP builder found for task '${taskType}'`)
|
|
return { xml: '', taskId: cwmpTask.id }
|
|
}
|
|
return { xml: builder(props), taskId: cwmpTask.id }
|
|
}
|
|
|
|
/**
|
|
* Registers a callback for a task already in the queue
|
|
*/
|
|
registerCallback(taskId: string, callback: (result: TaskResult) => Promise<string>): void {
|
|
this.taskCallbacks.set(taskId, callback)
|
|
}
|
|
|
|
/**
|
|
* Adds a CWMP task (internal)
|
|
*/
|
|
addCWMPTask(deviceId: DeviceId, type: CWMPTaskType, payload: CWMPTaskPayload): CWMPTask {
|
|
const taskId = this.generateTaskId(deviceId, type)
|
|
const task: CWMPTask = {
|
|
id: taskId,
|
|
deviceId,
|
|
type,
|
|
payload,
|
|
status: TaskStatus.PENDING,
|
|
createdAt: new Date(),
|
|
}
|
|
|
|
this.tasks.set(taskId, task)
|
|
|
|
const deviceKey = JSON.stringify(deviceId)
|
|
if (!this.deviceQueues.has(deviceKey)) {
|
|
this.deviceQueues.set(deviceKey, [])
|
|
}
|
|
this.deviceQueues.get(deviceKey)!.push(taskId)
|
|
|
|
this.emit('taskAdded', task)
|
|
return task
|
|
}
|
|
|
|
/**
|
|
* Retrieve and mark a pending task as SENT for a device
|
|
*/
|
|
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
|
|
|
|
const taskId = queue[0]
|
|
const cwmpTask = this.tasks.get(taskId)
|
|
if (!cwmpTask) return undefined
|
|
|
|
queue.shift()
|
|
|
|
if (cwmpTask.status === TaskStatus.PENDING) {
|
|
cwmpTask.status = TaskStatus.SENT
|
|
}
|
|
|
|
return { id: taskId, task: cwmpTask.type, props: cwmpTask.payload }
|
|
}
|
|
|
|
completeTask(taskId: string, result: CWMPTaskResultData): void {
|
|
const task = this.tasks.get(taskId)
|
|
if (!task) return
|
|
|
|
task.status = TaskStatus.COMPLETED
|
|
task.result = result
|
|
task.completedAt = new Date()
|
|
|
|
this.removeTaskFromQueue(task.deviceId, taskId)
|
|
this.emit('taskCompleted', task)
|
|
}
|
|
|
|
failTask(taskId: string, error: string): void {
|
|
const task = this.tasks.get(taskId)
|
|
if (!task) return
|
|
|
|
task.status = TaskStatus.FAILED
|
|
task.error = error
|
|
task.completedAt = new Date()
|
|
|
|
this.removeTaskFromQueue(task.deviceId, taskId)
|
|
this.emit('taskFailed', task)
|
|
}
|
|
|
|
async executeTaskCallback(taskId: string, status: ResultStatus, data: CWMPTaskResultData): Promise<string> {
|
|
const callback = this.taskCallbacks.get(taskId)
|
|
if (callback) {
|
|
const result = new TaskResult()
|
|
result.status = status
|
|
result.data = data
|
|
result.timestamp = new Date()
|
|
|
|
try {
|
|
const response = await callback(result)
|
|
this.taskCallbacks.delete(taskId)
|
|
return response
|
|
} catch (error) {
|
|
logger.error(`Error executing task callback: ${error}`)
|
|
this.taskCallbacks.delete(taskId)
|
|
return ''
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
|
|
getLastCreatedTaskId(deviceId: DeviceId): string | undefined {
|
|
return this.lastCreatedTaskIdByDevice.get(JSON.stringify(deviceId))
|
|
}
|
|
|
|
clearPendingTasksForDevice(deviceId: DeviceId): void {
|
|
const deviceKey = JSON.stringify(deviceId)
|
|
const queue = this.deviceQueues.get(deviceKey) || []
|
|
for (const taskId of queue) {
|
|
this.tasks.delete(taskId)
|
|
this.taskCallbacks.delete(taskId)
|
|
}
|
|
this.deviceQueues.set(deviceKey, [])
|
|
this.lastCreatedTaskIdByDevice.delete(deviceKey)
|
|
}
|
|
|
|
private generateTaskId(deviceId: DeviceId, type: string): string {
|
|
return `${JSON.stringify(deviceId)}_${type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
}
|
|
|
|
private removeTaskFromQueue(deviceId: DeviceId, taskId: string): void {
|
|
const deviceKey = JSON.stringify(deviceId)
|
|
const queue = this.deviceQueues.get(deviceKey)
|
|
if (queue) {
|
|
const index = queue.indexOf(taskId)
|
|
if (index > -1) queue.splice(index, 1)
|
|
}
|
|
}
|
|
}
|