Files
adam-acs/app/services/cwmp/device_action_executor.ts
T
Kevin L'Hereec f3eb8e1650 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)
2026-07-20 15:25:26 +02:00

429 lines
14 KiB
TypeScript

import { Device } from '../../domain/device/device.js'
import { DeviceParameter } from '../../domain/device/device_parameter.js'
import { ParameterType } from '../../domain/shared/types.js'
import { CWMPTaskQueue, ResultStatus, TaskResult } from './task_queue.js'
import DeviceModel from '../../models/device.js'
import logger from '@adonisjs/core/services/logger'
import type {
GetParameterValuesResponse,
GetParameterAttributesResponse,
GetParameterNamesResponse,
ParameterValueStruct,
ParameterAttributeStruct,
ParameterInfoStruct,
} from '../../protocol/tr069_types.js'
// ─── Action parameter interfaces (exported pour usage externe) ─────────────────
export interface SetParameterValue {
parameterName: string
value: string | number | boolean
type?: string
}
export interface SetParameterAttribute {
name: string
notificationChange: boolean
notification: number
accessListChange: boolean
accessList: string[]
}
export interface SetScheduleInform {
delayInSeconds: number
commandKey?: string
}
export interface DownloadParameter {
commandKey: string
fileType: string
url: string
username?: string
password?: string
fileSize?: number
targetFileName?: string
delaySeconds?: number
successUrl?: string
failureUrl?: string
}
export interface UploadParameter {
commandKey: string
fileType: string
url: string
username?: string
password?: string
delayInSeconds?: number
}
export interface FactoryResetParameter {
commandKey?: string
}
export interface RebootParameter {
commandKey?: string
}
/**
* Executes CWMP actions on devices via the TaskQueue
* Each action wraps a callback that persists state on success
*/
export class DeviceActionExecutor {
private taskQueue: CWMPTaskQueue
constructor(taskQueue: CWMPTaskQueue) {
this.taskQueue = taskQueue
}
private async persistDevice(device: Device): Promise<void> {
try {
const snapshot = device.toSnapshot()
await DeviceModel.createOrUpdate({
identifier: snapshot.identifier,
deviceId: snapshot.deviceId,
properties: snapshot.properties,
parameters: snapshot.parameters,
gpnStatus: snapshot.gpnStatus,
dataModel: snapshot.dataModel,
updatedAt: new Date(),
})
} catch (e) {
logger.error(`Failed to persist device: ${e}`)
}
}
/**
* Get parameter values from device.
*/
async getParameterValues(
device: Device,
parameterPaths: string[],
callback: (result: TaskResult) => Promise<string>
): Promise<string> {
const wrappedCallback = async (result: TaskResult) => {
if (result.status === ResultStatus.Success) {
try {
const resp = result.data as GetParameterValuesResponse
const list: ParameterValueStruct[] = resp?.ParameterList?.ParameterValueStruct ?? []
if (list.length > 0) {
device.updateFromValues(list)
await this.persistDevice(device)
}
} catch (e) {
logger.error(`Error updating device from parameter values: ${e}`)
}
}
return await callback(result)
}
return this.taskQueue.addTask(device.deviceId, 'get_parameter_values', parameterPaths, wrappedCallback).xml
}
async getParameterAttributes(
device: Device,
parameterList: string[],
callback: (result: TaskResult) => Promise<string>
): Promise<string> {
const wrappedCallback = async (result: TaskResult) => {
if (result.status === ResultStatus.Success) {
try {
const resp = result.data as GetParameterAttributesResponse
const list: ParameterAttributeStruct[] = resp?.ParameterList?.ParameterAttributeStruct ?? []
if (list.length > 0) {
for (const attr of list) {
const name: string | undefined =
typeof attr?.Name === 'string' ? attr.Name : (attr?.Name as { _?: string } | undefined)?._
if (!name) continue
const notification: number | undefined =
typeof attr?.Notification === 'number' ? attr.Notification : undefined
const accessList: string[] | undefined = attr?.AccessList?.string
const param = device.getParameter(name)
if (param) {
param.updateAttributes?.(notification, accessList)
continue
}
const prop = device.getProperty(name)
if (prop) {
prop.updateAttributes?.(notification, accessList)
}
}
await this.persistDevice(device)
}
} catch (e) {
logger.error(`Error updating device from parameter attributes: ${e}`)
}
}
return await callback(result)
}
return this.taskQueue.addTask(device.deviceId, 'get_parameter_attributes', parameterList, wrappedCallback).xml
}
async getParameterNames(
device: Device,
parameterName: string,
callback: (result: TaskResult) => Promise<string>
): Promise<string> {
const wrappedCallback = async (result: TaskResult) => {
if (result.status === ResultStatus.Success) {
try {
const resp = result.data as GetParameterNamesResponse
const list: ParameterInfoStruct[] = resp?.ParameterList?.ParameterInfoStruct ?? []
device.organizeParameters(list, new Date())
device.setGpnStatus('supported')
device.recordGpnSuccess()
device.lastGpnSentDate = new Date()
await this.persistDevice(device)
} catch (e) {
logger.error(`Error organizing parameters: ${e}`)
}
} else {
// Inspect fault code to distinguish "unsupported" from "transient error"
const faultData = result.data as { FaultCode?: number | string; faultCode?: number | string; Detail?: { Fault?: { FaultCode?: number | string } }; detail?: { Fault?: { FaultCode?: number | string } } }
const faultCode =
faultData?.FaultCode ??
faultData?.faultCode ??
faultData?.Detail?.Fault?.FaultCode ??
faultData?.detail?.Fault?.FaultCode
const code = typeof faultCode === 'string' ? parseInt(faultCode, 10) : faultCode
const GPN_UNSUPPORTED_CODES = [9000, 9001, 9003, 9004, 9005]
const GPN_TRANSIENT_CODES = [9002]
if (code !== undefined && GPN_UNSUPPORTED_CODES.includes(code)) {
device.setGpnStatus('unsupported')
device.recordGpnFailure()
await this.persistDevice(device)
} else if (code !== undefined && GPN_TRANSIENT_CODES.includes(code)) {
logger.warn({ code, deviceId: device.deviceId?.serialNumber }, 'GPN transient error (9002) — will not mark unsupported')
}
}
return await callback(result)
}
return this.taskQueue.addTask(device.deviceId, 'get_parameter_names', parameterName, wrappedCallback).xml
}
async applySetParameterValues(
device: Device,
parameters: SetParameterValue[],
callback: (result: boolean) => Promise<string>
): Promise<string> {
const existingParameters = device.getParameters()
const inferType = (val: string | number | boolean): string => {
const t = typeof val
switch (t) {
case 'boolean': return 'xsd:boolean'
case 'number': return Number.isInteger(val) ? 'xsd:int' : 'xsd:string'
default: return 'xsd:string'
}
}
const cwmpTypeMap: Record<string, string> = {
string: 'xsd:string',
unsignedInt: 'xsd:unsignedInt',
int: 'xsd:int',
boolean: 'xsd:boolean',
dateTime: 'xsd:dateTime',
}
const pvStructs = parameters.map((p) => {
const existing = existingParameters.find((ep) => ep.name === p.parameterName)
let xsdType: string
if (p.type && cwmpTypeMap[p.type]) {
xsdType = cwmpTypeMap[p.type]
} else if (existing?.type && existing.type !== ParameterType.UNKNOWN) {
xsdType = existing.type
} else {
xsdType = inferType(p.value)
}
return {
Name: p.parameterName,
Value: { _: p.value as string, $: { Type: xsdType } },
}
})
const parameterKey = `SetPValues_${device.deviceId.serialNumber}_${Date.now()}`
return this.taskQueue.addTask(
device.deviceId,
'set_parameter_values',
{ parameterValueList: pvStructs, parameterKey },
async (result: TaskResult) => {
if (result.status === ResultStatus.Success) {
device.updateFromValues(pvStructs as ParameterValueStruct[])
await this.persistDevice(device)
return await callback(true)
}
return await callback(false)
}
).xml
}
async applySetParameterAttributes(
device: Device,
parameters: SetParameterAttribute[],
callback: (result: boolean) => Promise<string>
): Promise<string> {
const attrs = parameters.map((p) => ({
name: p.name,
notificationChange: p.notificationChange ?? false,
notification: p.notification ?? 0,
accessListChange: p.accessListChange ?? false,
accessList: (p.accessList as unknown as { string?: string[] })?.string || p.accessList || [],
}))
return this.taskQueue.addTask(
device.deviceId,
'set_parameter_attributes',
attrs,
async (result: TaskResult) => {
if (result.status === ResultStatus.Success) {
return await callback(true)
}
return await callback(false)
}
).xml
}
async applyScheduleInform(
device: Device,
parameter: SetScheduleInform,
callback: (result: boolean) => Promise<string>
): Promise<string> {
const commandKey = parameter.commandKey || `ScheduleInform_${device.deviceId.serialNumber}_${Date.now()}`
return this.taskQueue.addTask(
device.deviceId,
'schedule_inform',
{ delayInSeconds: parameter.delayInSeconds, commandKey },
async (result: TaskResult) => {
return await callback(result.status === ResultStatus.Success)
}
).xml
}
async applyFactoryReset(
_device: Device,
_parameter: FactoryResetParameter,
callback: (result: boolean) => Promise<string>
): Promise<string> {
return this.taskQueue.addTask(
_device.deviceId,
'factory_reset',
{} as Record<string, never>,
async (result: TaskResult) => {
return await callback(result.status === ResultStatus.Success)
}
).xml
}
async applyReboot(
device: Device,
parameter: RebootParameter,
callback: (result: boolean) => Promise<string>
): Promise<string> {
return this.taskQueue.addTask(
device.deviceId,
'reboot',
{ commandKey: parameter.commandKey || '' },
async (result: TaskResult) => {
return await callback(result.status === ResultStatus.Success)
}
).xml
}
async applyDownload(
device: Device,
parameter: DownloadParameter,
callback: (result: boolean) => Promise<string>
): Promise<string> {
return this.taskQueue.addTask(
device.deviceId,
'download',
{
commandKey: parameter.commandKey,
fileType: parameter.fileType,
url: parameter.url,
username: parameter.username,
password: parameter.password,
fileSize: parameter.fileSize,
targetFileName: parameter.targetFileName,
delaySeconds: parameter.delaySeconds,
successUrl: parameter.successUrl,
failureUrl: parameter.failureUrl,
},
async (result: TaskResult) => {
return await callback(result.status === ResultStatus.Success)
}
).xml
}
async applyUpload(
device: Device,
parameter: UploadParameter,
callback: (result: boolean) => Promise<string>
): Promise<string> {
return this.taskQueue.addTask(
device.deviceId,
'upload',
{
commandKey: parameter.commandKey,
fileType: parameter.fileType,
url: parameter.url,
username: parameter.username,
password: parameter.password,
delaySeconds: parameter.delayInSeconds,
},
async (result: TaskResult) => {
return await callback(result.status === ResultStatus.Success)
}
).xml
}
async insertProvisioningCode(
device: Device,
callback: (result: boolean) => Promise<string>
): Promise<string> {
const allParams = device.getParameters()
const allProps = device.getProperties()
const knownPC = [...allParams, ...allProps].find((p) => /(^|\.)ProvisioningCode$/.test(p?.name))
let provisioningPath: string
if (knownPC) {
provisioningPath = knownPC.name
} else if (device.dataModel === 'tr-181') {
provisioningPath = 'Device.DeviceInfo.ProvisioningCode'
} else if (device.dataModel === 'tr-098') {
provisioningPath = 'InternetGatewayDevice.DeviceInfo.ProvisioningCode'
} else {
const hasDeviceTree = [...allParams, ...allProps].some(
(p) => typeof p?.name === 'string' && p.name.startsWith('Device.')
)
provisioningPath = hasDeviceTree
? 'Device.DeviceInfo.ProvisioningCode'
: 'InternetGatewayDevice.DeviceInfo.ProvisioningCode'
}
const parameterKey = `SetPValues_${device.deviceId.serialNumber}_${Date.now()}`
const pvStructs = [{ Name: provisioningPath, Value: { _: device.tmpPC as string, $: { Type: 'xsd:string' } } }]
return this.taskQueue.addTask(
device.deviceId,
'set_parameter_values',
{ parameterValueList: pvStructs, parameterKey },
async (result: TaskResult) => {
if (result.status === ResultStatus.Success) {
device.removeProperty(provisioningPath)
device.setParameter(new DeviceParameter(provisioningPath, device.tmpPC, ParameterType.STRING))
await this.persistDevice(device)
device.tmpPC = ''
return await callback(true)
}
return await callback(false)
}
).xml
}
}