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

+65 -64
View File
@@ -1,5 +1,4 @@
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, detectDataModelFromParams } from '../domain/device/device.js'
@@ -10,6 +9,9 @@ import { getDeviceHashIdentifier } from '../utils/device_utils.js'
import { getReferenceEngine } from '../services/references/reference_engine.js'
import { normalizeEventCode } from '../utils/cwmp-events.js'
import logger from '@adonisjs/core/services/logger'
import type { Inform, CwmpMessage, ParameterValueStruct } from '../protocol/tr069_types.js'
import { ParameterType } from '../domain/shared/types.js'
import { FaultCodeType } from '../protocol/tr069_types.js'
export default class CwmpController {
private static managerInitialized = false
@@ -40,10 +42,8 @@ export default class CwmpController {
try {
if (isEmpty) {
const rebound = manager.ensureSessionForRemote(remoteKey, sessionId)
// Try exact remoteKey first (same TCP connection), then fall back to IP-only
// (CPE may open a new TCP port for the empty POST)
const hashId = manager.getHashIdForRemote(remoteKey)
?? manager.getHashIdForRemoteIp(request.ip())
const hashId =
manager.getHashIdForRemote(remoteKey) ?? manager.getHashIdForRemoteIp(request.ip())
if (hashId) ctx.cwmpDeviceId = hashId
logger.debug(
@@ -66,8 +66,9 @@ export default class CwmpController {
)
if (message?.type === 'Inform') {
const informMsg = message as Extract<CwmpMessage, { type: 'Inform' }>
const { xml, hashId } = await this.handleInform(
message.data,
informMsg.data,
sessionId,
envelope?.header?.id,
remoteKey,
@@ -80,20 +81,16 @@ export default class CwmpController {
}
if (message?.type === 'TransferComplete') {
const hashId = manager.getHashIdForRemote(remoteKey)
?? manager.getHashIdForRemoteIp(request.ip())
const hashId =
manager.getHashIdForRemote(remoteKey) ?? manager.getHashIdForRemoteIp(request.ip())
if (hashId) ctx.cwmpDeviceId = hashId
logger.debug({ sessionId, hashId }, 'CWMP sending TransferCompleteResponse')
const resp = buildSoapEnvelope(
undefined,
'<cwmp:TransferCompleteResponse></cwmp:TransferCompleteResponse>'
)
return this.sendSoap(response, resp)
return this.sendSoap(response, CWMPMessageBuilder.transferCompleteResponse())
}
manager.ensureSessionForRemote(remoteKey, sessionId)
const hashId = manager.getHashIdForRemote(remoteKey)
?? manager.getHashIdForRemoteIp(request.ip())
const hashId =
manager.getHashIdForRemote(remoteKey) ?? manager.getHashIdForRemoteIp(request.ip())
if (hashId) ctx.cwmpDeviceId = hashId
if (message && manager.hasSession(sessionId)) {
@@ -104,15 +101,19 @@ export default class CwmpController {
)
const result = await manager.handleCWMPResponse(sessionId, success, payload)
if (result && result !== '') {
logger.debug({ sessionId, hashId, responseLength: result.length }, 'CWMP state machine produced response')
logger.debug(
{ sessionId, hashId, responseLength: result.length },
'CWMP state machine produced response'
)
return this.sendSoap(response, result)
}
}
return response.status(204).send('')
} catch (error: any) {
logger.error({ error: error?.message, stack: error?.stack }, 'CWMP error handling request')
const faultXml = this.buildSoapFault(error?.message || 'Internal error')
} catch (error: unknown) {
const err = error as Error
logger.error({ error: err?.message, stack: err?.stack }, 'CWMP error handling request')
const faultXml = this.buildSoapFault(err?.message || 'Internal error')
return this.sendSoap(response, faultXml)
}
}
@@ -120,7 +121,7 @@ export default class CwmpController {
// ─── Inform Handling ───────────────────────────────────────────────────────
private async handleInform(
informData: any,
informData: Inform,
session: string,
cwmpId: string | undefined,
remoteKey: string,
@@ -131,7 +132,8 @@ export default class CwmpController {
{ informKeys: Object.keys(informData || {}), deviceId: informData?.DeviceId },
'CWMP handleInform called'
)
const deviceIdStruct = informData?.DeviceId || {}
const deviceIdStruct = informData?.DeviceId
const deviceId = DeviceId.fromInform(deviceIdStruct)
// Load or create device
@@ -148,19 +150,17 @@ export default class CwmpController {
device.updateLastSeen()
// Process inform parameter values
const paramValues = informData?.ParameterList?.ParameterValueStruct || []
const paramValues: ParameterValueStruct[] =
informData?.ParameterList?.ParameterValueStruct || []
if (paramValues.length > 0) {
device.updateFromValues(paramValues)
// Some writable parameters arrive in the Inform before GPN has classified them.
// updateFromValues falls back to storing unknowns as read-only properties.
// Reclassify known-writable Inform parameters as parameters (writable).
device.reclassifyInformWritables(paramValues)
}
device.setTmpParamValues(paramValues)
// Detect TR data model from Inform parameter names (Device.* = TR-181, InternetGatewayDevice.* = TR-098)
// Only update if currently unknown — once detected, keep it stable across sessions
// Detect TR data model
if (device.dataModel === 'unknown') {
const detectedModel = detectDataModelFromParams(paramValues)
if (detectedModel !== 'unknown') {
@@ -192,24 +192,26 @@ export default class CwmpController {
const rawEventStruct = informData?.Event?.EventStruct
const eventStructArray = Array.isArray(rawEventStruct)
? rawEventStruct
: rawEventStruct ? [rawEventStruct] : []
: rawEventStruct
? [rawEventStruct]
: []
const events: string[] = eventStructArray
.map((e: any) => e?.EventCode)
.filter((c: any) => c != null)
.map((c: any) => String(c))
.map((e) => e?.EventCode)
.filter((c) => c != null)
.map((c) => String(c))
logger.info({ events }, 'CWMP Inform events extracted')
// Track last periodic inform timestamp + last remote IP (used for Connection Request fallback)
// Track last periodic inform timestamp + last remote IP
const normalizedEvents = events.map((e) => normalizeEventCode(e))
const isPeriodic = normalizedEvents.some((e) => e === 'PERIODIC')
const isBootstrap = normalizedEvents.some((e) => e === 'BOOTSTRAP')
const cpeIpForStorage = (xForwardedFor || '').split(',')[0]?.trim() || remoteKey.split(':')[0]
const dbUpdate: Record<string, any> = { lastRemoteIp: cpeIpForStorage }
const dbUpdate: Record<string, unknown> = { lastRemoteIp: cpeIpForStorage }
if (isPeriodic || isBootstrap) dbUpdate.lastPeriodicInformAt = new Date()
await DeviceModel.updateOne({ identifier: hashId }, { $set: dbUpdate }).catch(() => {})
// Phase 12: Crash pattern detection
// Crash pattern detection
const hasBoot = normalizedEvents.some((e) => e === 'BOOT')
if (hasBoot && device.lastGpnSentDate) {
const timeSinceGpn = Date.now() - device.lastGpnSentDate.getTime()
@@ -223,24 +225,31 @@ export default class CwmpController {
await manager.startStateMachine(device, session, { events })
manager.bindRemoteToDevice(remoteKey, device.deviceId)
// Resolve references from configured providers (async, non-blocking)
// Resolve references (async, non-blocking)
try {
logger.debug({ hashId, events, xForwardedFor }, '[References] Starting resolution')
const engine = getReferenceEngine()
// Use x-forwarded-for header (set by simulator) or fall back to TCP remote IP
const cpeIp = (xForwardedFor || '').split(',')[0]?.trim() || remoteKey.split(':')[0]
const informContext = { events, remoteIp: cpeIp, headers: {} }
const references = await engine.resolveAll(
{ identifier: hashId, deviceId, properties: device.getProperties().map((p) => p.toSnapshot()), parameters: device.getParameters().map((p) => p.toSnapshot()) },
{
identifier: hashId,
deviceId,
properties: device.getProperties().map((p) => p.toSnapshot()),
parameters: device.getParameters().map((p) => p.toSnapshot()),
},
informContext
)
if (references.length > 0) {
await DeviceModel.updateOne({ identifier: hashId }, { $set: { references, updatedAt: new Date() } }).catch(() => {})
await DeviceModel.updateOne(
{ identifier: hashId },
{ $set: { references, updatedAt: new Date() } }
).catch(() => {})
logger.debug({ hashId, providers: references.map((r) => r.provider) }, '[References] Resolved')
}
} catch (_e: any) {
// Non-critical — don't block Inform processing
logger.error({ error: _e?.message, stack: _e?.stack }, '[References] Error during resolution')
} catch (_e: unknown) {
const err = _e as Error
logger.error({ error: err?.message, stack: err?.stack }, '[References] Error during resolution')
}
return { xml: CWMPMessageBuilder.informResponse(cwmpId), hashId }
@@ -248,32 +257,21 @@ export default class CwmpController {
// ─── Helpers ───────────────────────────────────────────────────────────────
private sendSoap(response: any, xml: string) {
private sendSoap(response: HttpContext['response'], 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']
private getMessageInfo(message: CwmpMessage): { success: boolean; payload: CwmpMessage['data'] } {
return {
success: !failTypes.includes(message.type),
success: message.type !== 'Fault',
payload: message.data,
}
}
private buildSoapFault(reason: string): string {
const body = `<soap-env:Fault>
<faultcode>Server</faultcode>
<faultstring>${reason}</faultstring>
<detail>
<cwmp:Fault>
<FaultCode>8002</FaultCode>
<FaultString>${reason}</FaultString>
</cwmp:Fault>
</detail>
</soap-env:Fault>`
return buildSoapEnvelope(undefined, body)
return CWMPMessageBuilder.soapFault(FaultCodeType.ACSInternalError, reason)
}
private async loadDevice(deviceId: DeviceId): Promise<Device | null> {
@@ -291,22 +289,25 @@ export default class CwmpController {
record.createdAt
)
record.properties?.forEach((p: any) =>
device.setProperty(new DeviceProperty(p.name, p.value, p.type))
record.properties?.forEach((p: { name: string; value: unknown; type: string }) =>
device.setProperty(new DeviceProperty(p.name, p.value, (p.type as ParameterType) ?? ParameterType.UNKNOWN))
)
record.parameters?.forEach((p: any) =>
device.setParameter(new DeviceParameter(p.name, p.value, p.type))
record.parameters?.forEach((p: { name: string; value: unknown; type: string }) =>
device.setParameter(new DeviceParameter(p.name, p.value, (p.type as ParameterType) ?? ParameterType.UNKNOWN))
)
device.setGpnStatus(record.gpnStatus ?? 'unknown')
device.setDataModel(record.dataModel ?? 'unknown')
// Restore circuit breaker state
if (record.gpnCircuitBreaker) {
device.setGpnCircuitBreaker({
failureCount: record.gpnCircuitBreaker.failureCount ?? 0,
lastFailureDate: record.gpnCircuitBreaker.lastFailureDate ? new Date(record.gpnCircuitBreaker.lastFailureDate) : null,
lastFailureDate: record.gpnCircuitBreaker.lastFailureDate
? new Date(record.gpnCircuitBreaker.lastFailureDate)
: null,
state: record.gpnCircuitBreaker.state ?? 'closed',
cooldownUntil: record.gpnCircuitBreaker.cooldownUntil ? new Date(record.gpnCircuitBreaker.cooldownUntil) : null,
cooldownUntil: record.gpnCircuitBreaker.cooldownUntil
? new Date(record.gpnCircuitBreaker.cooldownUntil)
: null,
})
}
if (record.lastGpnSentDate) {
@@ -314,7 +315,7 @@ export default class CwmpController {
}
return device
} catch (_error) {
} catch {
return null
}
}
+20 -18
View File
@@ -2,6 +2,7 @@ import { DeviceId } from './device_id.js'
import { DeviceParameter, DeviceProperty, ParameterSnapshot, PropertySnapshot } from './device_parameter.js'
import { ParameterType } from '../shared/types.js'
import { createHash } from 'node:crypto'
import type { ParameterValueStruct, ParameterInfoStruct } from '../../protocol/tr069_types.js'
export type GpnStatus = 'supported' | 'unsupported' | 'unknown'
@@ -16,14 +17,15 @@ export interface GpnCircuitBreaker {
export type DataModel = 'tr-181' | 'tr-098' | 'unknown'
/** Extract a string name from a SOAP parsed node (may be a string or an object with _ text node) */
function extractName(raw: any): string | null {
function extractName(raw: string | { _: string } | unknown): string | null {
if (typeof raw === 'string') return raw || null
if (raw && typeof raw === 'object' && typeof raw._ === 'string') return raw._ || null
if (raw && typeof raw === 'object' && typeof (raw as { _?: unknown })._ === 'string')
return (raw as { _: string })._ || null
return null
}
/** Detect which TR data model a device uses from a ParameterValueStruct list */
export function detectDataModelFromParams(paramValues: any[]): DataModel {
export function detectDataModelFromParams(paramValues: ParameterValueStruct[]): DataModel {
for (const pv of paramValues ?? []) {
const name = extractName(pv?.Name)
if (!name) continue
@@ -42,8 +44,8 @@ export enum OnboardingStatus {
}
export class Device {
public tmpParamValues: any[] = []
public tmpPC: any = null
public tmpParamValues: ParameterValueStruct[] = []
public tmpPC: string | null = null
private _parameters: Map<string, DeviceParameter> = new Map()
private _properties: Map<string, DeviceProperty> = new Map()
private _session: any = null
@@ -268,13 +270,13 @@ export class Device {
* - *.ProvisioningCode
* - *.ParameterKey
*/
public reclassifyInformWritables(paramValues: any[]): void {
public reclassifyInformWritables(paramValues: ParameterValueStruct[]): void {
const WRITABLE_PATTERNS = [
/(^|\.)ProvisioningCode$/,
/(^|\.)ParameterKey$/,
]
for (const pv of paramValues || []) {
const name = typeof pv?.Name === 'string' ? pv.Name : pv?.Name?._ || null
const name = typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || null
if (!name) continue
if (!WRITABLE_PATTERNS.some((re) => re.test(name))) continue
// Only act if it ended up as a property (wrong classification)
@@ -297,19 +299,19 @@ export class Device {
return this.isOnline() && this._onboardingStatus === OnboardingStatus.COMPLETED
}
public setTmpParamValues(values: any[]): void {
public setTmpParamValues(values: ParameterValueStruct[]): void {
this.tmpParamValues = values
}
public getTmpParamValues(): any[] {
public getTmpParamValues(): ParameterValueStruct[] {
return this.tmpParamValues
}
public setTmpPC(value: any): void {
public setTmpPC(value: string | null): void {
this.tmpPC = value
}
public getTmpPC(): any {
public getTmpPC(): string | null {
return this.tmpPC
}
@@ -318,8 +320,7 @@ export class Device {
this.tmpPC = null
}
public organizeParameters(response: any, timestamp: Date): void {
const infoList = response?.ParameterList?.ParameterInfoStruct || []
public organizeParameters(infoList: ParameterInfoStruct[], timestamp: Date): void {
for (const info of infoList) {
const name = extractName(info?.Name)
if (!name) continue
@@ -349,14 +350,15 @@ export class Device {
this._lastSeen = timestamp
}
public updateFromValues(paramValues: any[]): void {
public updateFromValues(paramValues: ParameterValueStruct[]): void {
for (const pv of paramValues || []) {
const name = extractName(pv?.Name)
const valueObj = pv?.Value
const hasTextNode = valueObj && Object.prototype.hasOwnProperty.call(valueObj as any, '_')
const hasTypeAttr = valueObj && (valueObj['$xsi:type'] || valueObj?.$?.Type)
const val = hasTextNode ? valueObj._ : (hasTypeAttr ? '' : undefined)
const typeStr = valueObj?.['$xsi:type'] || valueObj?.$?.Type
const hasTextNode = valueObj && Object.prototype.hasOwnProperty.call(valueObj, '_')
const valueObjRaw = valueObj as unknown as Record<string, unknown> | undefined
const hasTypeAttr = valueObjRaw && (valueObjRaw['$xsi:type'] || valueObj?.$?.Type)
const val = hasTextNode ? valueObj?._ : (hasTypeAttr ? '' : undefined)
const typeStr = (valueObjRaw?.['$xsi:type'] as string | undefined) || valueObj?.$?.Type
const mappedType = this.mapType(typeStr)
if (!name) continue
+1 -1
View File
@@ -30,7 +30,7 @@ export class DeviceId {
)
}
public static fromInform(informData: any): DeviceId {
public static fromInform(informData: { Manufacturer?: string; OUI?: string; ProductClass?: string; SerialNumber?: string }): DeviceId {
return new DeviceId(
String(informData?.Manufacturer ?? ''),
String(informData?.OUI ?? ''),
+4 -4
View File
@@ -1,7 +1,7 @@
import { DeviceId } from './device_id.js'
import { PropertySnapshot, ParameterSnapshot } from './device_parameter.js'
import { DeviceComplianceInfo } from '../configuration/compliance.js'
import { DataModel, GpnStatus, GpnCircuitBreaker } from './device.js'
import { type DeviceId } from './device_id.js'
import { type PropertySnapshot, type ParameterSnapshot } from './device_parameter.js'
import { type DeviceComplianceInfo } from '../configuration/compliance.js'
import { type DataModel, type GpnStatus, type GpnCircuitBreaker } from './device.js'
import type { DeviceReference } from '../../services/references/types.js'
export interface DeviceRecord {
+7 -5
View File
@@ -1,6 +1,7 @@
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
import { parseSoapXml, extractCwmpMessage } from '../protocol/soap.js'
import type { SoapEnvelope, CwmpMessage } from '../protocol/tr069_types.js'
/**
* CWMP Middleware
@@ -81,12 +82,13 @@ export default class CwmpMiddleware {
cwmpVersion: envelope.cwmpVersion,
cwmpId: envelope.header?.id,
messageType: message?.type ?? 'unknown',
messageKeys: message?.data ? Object.keys(message.data) : [],
messageKeys: message?.data && typeof message.data === 'object' ? Object.keys(message.data as object) : [],
},
'CWMP XML parsed'
)
} catch (error: any) {
logger.error({ sessionId, error: error.message }, 'CWMP XML parse error')
} catch (error: unknown) {
const msg = error instanceof Error ? error.message : String(error)
logger.error({ sessionId, error: msg }, 'CWMP XML parse error')
response.status(400).send('Malformed SOAP/XML')
return
}
@@ -104,8 +106,8 @@ declare module '@adonisjs/core/http' {
sessionId: string
remoteKey: string
rawBody: string
envelope: any | null
message: { type: string; data: any } | null
envelope: SoapEnvelope | null
message: CwmpMessage | null
isEmpty: boolean
}
}
+157 -112
View File
@@ -1,147 +1,192 @@
import { buildSoapEnvelope } from '../soap.js'
import { randomBytes } from 'node:crypto'
import {
Cwmp_1_0,
Download,
FactoryReset,
FaultCodeType,
FaultDetail,
FaultStruct,
Fault,
GetParameterAttributes,
GetParameterNames,
GetParameterValues,
InformResponse,
ParameterValueList,
Reboot,
ScheduleInform,
SetParameterAttributes,
SetParameterAttributesList,
SetParameterAttributesStruct,
AccessList,
SetParameterValues,
TransferCompleteResponse,
Upload,
} from '../cwmp-1-0.js'
import {
CwmpID,
NAMESPACES,
SoapEnvelope,
SoapEnvelopeAttribute,
SoapEnvelopeHeader,
} from '../soap_envelope.js'
import type { DownloadTaskPayload, UploadTaskPayload, SetParameterValuesTaskPayload, SetParameterAttributesTaskPayload } from '../tr069_types.js'
function generateCwmpID(): string {
return randomBytes(4).toString('hex')
}
function makeEnvelope(cwmpId?: string): SoapEnvelope {
const enve = new SoapEnvelope()
enve.$ = NAMESPACES['1.0'] as SoapEnvelopeAttribute
enve.Header = new SoapEnvelopeHeader()
enve.Header.ID = new CwmpID({
$: { MustUnderstand: '1' },
_: cwmpId || generateCwmpID(),
})
enve.Body = new Cwmp_1_0()
return enve
}
export class CWMPMessageBuilder {
static informResponse(cwmpId?: string): string {
const body = `<cwmp:InformResponse>
<MaxEnvelopes>1</MaxEnvelopes>
</cwmp:InformResponse>`
return buildSoapEnvelope(cwmpId, body)
const enve = makeEnvelope(cwmpId)
enve.Body.InformResponse = new InformResponse({ MaxEnvelopes: 1 })
return enve.getXml()
}
static getParameterNames(cwmpId: string | undefined, parameterPath: string, nextLevel: boolean = true): string {
const body = `<cwmp:GetParameterNames>
<ParameterPath>${parameterPath}</ParameterPath>
<NextLevel>${nextLevel ? '1' : '0'}</NextLevel>
</cwmp:GetParameterNames>`
return buildSoapEnvelope(cwmpId, body)
const enve = makeEnvelope(cwmpId)
enve.Body.GetParameterNames = new GetParameterNames({
ParameterPath: parameterPath,
NextLevel: nextLevel,
})
return enve.getXml()
}
static getParameterValues(cwmpId: string | undefined, parameterNames: string[]): string {
const names = parameterNames.map((n) => `<string>${n}</string>`).join('\n ')
const body = `<cwmp:GetParameterValues>
<ParameterNames soap-enc:arrayType="xsd:string[${parameterNames.length}]">
${names}
</ParameterNames>
</cwmp:GetParameterValues>`
return buildSoapEnvelope(cwmpId, body)
const enve = makeEnvelope(cwmpId)
enve.Body.GetParameterValues = new GetParameterValues({ ParameterNames: parameterNames })
return enve.getXml()
}
static setParameterValues(cwmpId: string | undefined, parameters: { name: string; value: string; type: string }[]): string {
const params = parameters
.map(
(p) => `<ParameterValueStruct>
<Name>${p.name}</Name>
<Value xsi:type="${p.type}">${p.value}</Value>
</ParameterValueStruct>`
)
.join('\n ')
const body = `<cwmp:SetParameterValues>
<ParameterList soap-enc:arrayType="cwmp:ParameterValueStruct[${parameters.length}]">
${params}
</ParameterList>
<ParameterKey>papaye-v2</ParameterKey>
</cwmp:SetParameterValues>`
return buildSoapEnvelope(cwmpId, body)
}
static download(
static setParameterValues(
cwmpId: string | undefined,
params: {
commandKey?: string
fileType?: string
url: string
username?: string
password?: string
fileSize?: number
targetFileName?: string
delaySeconds?: number
successUrl?: string
failureUrl?: string
}
parameters: Array<{ name: string; value: string; type: string }>
): string {
const body = `<cwmp:Download>
<CommandKey>${params.commandKey || ''}</CommandKey>
<FileType>${params.fileType || '1 Firmware Upgrade Image'}</FileType>
<URL>${params.url}</URL>
<Username>${params.username || ''}</Username>
<Password>${params.password || ''}</Password>
<FileSize>${params.fileSize || 0}</FileSize>
<TargetFileName>${params.targetFileName || ''}</TargetFileName>
<DelaySeconds>${params.delaySeconds || 0}</DelaySeconds>
<SuccessURL>${params.successUrl || ''}</SuccessURL>
<FailureURL>${params.failureUrl || ''}</FailureURL>
</cwmp:Download>`
return buildSoapEnvelope(cwmpId, body)
const pvStructs = parameters.map((p) => ({
Name: p.name,
Value: { _: p.value, $: { Type: p.type } },
}))
const pvList = new ParameterValueList({
$: { ArrayType: `cwmp:ParameterValueStruct[${pvStructs.length}]` },
ParameterValueStruct: pvStructs,
})
const enve = makeEnvelope(cwmpId)
enve.Body.SetParameterValues = new SetParameterValues({
ParameterValueList: pvList,
ParameterKey: 'papaye-v2',
})
return enve.getXml()
}
static upload(
cwmpId: string | undefined,
params: { commandKey?: string; fileType?: string; url: string; username?: string; password?: string; delaySeconds?: number }
): string {
const body = `<cwmp:Upload>
<CommandKey>${params.commandKey || ''}</CommandKey>
<FileType>${params.fileType || '3 Vendor Configuration File'}</FileType>
<URL>${params.url}</URL>
<Username>${params.username || ''}</Username>
<Password>${params.password || ''}</Password>
<DelaySeconds>${params.delaySeconds || 0}</DelaySeconds>
</cwmp:Upload>`
return buildSoapEnvelope(cwmpId, body)
static setParameterValuesFromPayload(cwmpId: string | undefined, payload: SetParameterValuesTaskPayload): string {
const params = (payload.parameterValueList || []).map((p) => ({
name: p.Name,
value: String(p.Value?._ ?? ''),
type: p.Value?.$?.Type || 'xsd:string',
}))
return CWMPMessageBuilder.setParameterValues(cwmpId, params)
}
static download(cwmpId: string | undefined, params: DownloadTaskPayload): string {
const enve = makeEnvelope(cwmpId)
enve.Body.Download = new Download({
CommandKey: params.commandKey || '',
FileType: params.fileType || '1 Firmware Upgrade Image',
URL: params.url,
Username: params.username || '',
Password: params.password || '',
FileSize: params.fileSize || 0,
TargetFileName: params.targetFileName || '',
DelaySeconds: params.delaySeconds || 0,
SuccessURL: params.successUrl || '',
FailureURL: params.failureUrl || '',
})
return enve.getXml()
}
static upload(cwmpId: string | undefined, params: UploadTaskPayload): string {
const enve = makeEnvelope(cwmpId)
enve.Body.Upload = new Upload({
CommandKey: params.commandKey || '',
FileType: params.fileType || '3 Vendor Configuration File',
URL: params.url,
Username: params.username || '',
Password: params.password || '',
DelaySeconds: params.delaySeconds || 0,
})
return enve.getXml()
}
static reboot(cwmpId?: string, commandKey?: string): string {
const body = `<cwmp:Reboot>
<CommandKey>${commandKey || ''}</CommandKey>
</cwmp:Reboot>`
return buildSoapEnvelope(cwmpId, body)
const enve = makeEnvelope(cwmpId)
enve.Body.Reboot = new Reboot({ CommandKey: commandKey || '' })
return enve.getXml()
}
static factoryReset(cwmpId?: string): string {
const body = `<cwmp:FactoryReset></cwmp:FactoryReset>`
return buildSoapEnvelope(cwmpId, body)
const enve = makeEnvelope(cwmpId)
enve.Body.FactoryReset = new FactoryReset({})
return enve.getXml()
}
static scheduleInform(cwmpId: string | undefined, delaySeconds: number, commandKey?: string): string {
const body = `<cwmp:ScheduleInform>
<DelaySeconds>${delaySeconds}</DelaySeconds>
<CommandKey>${commandKey || ''}</CommandKey>
</cwmp:ScheduleInform>`
return buildSoapEnvelope(cwmpId, body)
const enve = makeEnvelope(cwmpId)
enve.Body.ScheduleInform = new ScheduleInform({
DelaySeconds: delaySeconds,
CommandKey: commandKey || '',
})
return enve.getXml()
}
static getParameterAttributes(cwmpId: string | undefined, parameterNames: string[]): string {
const names = parameterNames.map((n) => `<string>${n}</string>`).join('\n ')
const body = `<cwmp:GetParameterAttributes>
<ParameterNames soap-enc:arrayType="xsd:string[${parameterNames.length}]">
${names}
</ParameterNames>
</cwmp:GetParameterAttributes>`
return buildSoapEnvelope(cwmpId, body)
const enve = makeEnvelope(cwmpId)
enve.Body.GetParameterAttributes = new GetParameterAttributes({ ParameterNames: parameterNames })
return enve.getXml()
}
static setParameterAttributes(
cwmpId: string | undefined,
attributes: { name: string; notificationChange: boolean; notification: number; accessListChange: boolean; accessList: string[] }[]
attributes: SetParameterAttributesTaskPayload[]
): string {
const attrs = attributes
.map(
(a) => `<SetParameterAttributesStruct>
<Name>${a.name}</Name>
<NotificationChange>${a.notificationChange ? '1' : '0'}</NotificationChange>
<Notification>${a.notification}</Notification>
<AccessListChange>${a.accessListChange ? '1' : '0'}</AccessListChange>
<AccessList soap-enc:arrayType="xsd:string[${a.accessList.length}]">
${a.accessList.map((s) => `<string>${s}</string>`).join('')}
</AccessList>
</SetParameterAttributesStruct>`
)
.join('')
const body = `<cwmp:SetParameterAttributes>
<ParameterList soap-enc:arrayType="cwmp:SetParameterAttributesStruct[${attributes.length}]">
${attrs}
</ParameterList>
</cwmp:SetParameterAttributes>`
return buildSoapEnvelope(cwmpId, body)
const structs = attributes.map(
(a) =>
new SetParameterAttributesStruct({
Name: a.name,
NotificationChange: a.notificationChange,
Notification: a.notification,
AccessListChange: a.accessListChange,
AccessList: new AccessList({ string: a.accessList }),
})
)
const list = new SetParameterAttributesList({ SetParameterAttributesStruct: structs })
const enve = makeEnvelope(cwmpId)
enve.Body.SetParameterAttributes = new SetParameterAttributes({ ParameterList: list })
return enve.getXml()
}
static transferCompleteResponse(cwmpId?: string): string {
const enve = makeEnvelope(cwmpId)
enve.Body.TransferCompleteResponse = new TransferCompleteResponse({})
return enve.getXml()
}
static soapFault(faultCode: FaultCodeType, reason: string): string {
const fault = new Fault()
fault.Faultcode = 'Server'
fault.Faultstring = reason
fault.Detail = new FaultDetail({ Fault: new FaultStruct({ FaultCode: faultCode, FaultString: reason }) })
return fault.getXml()
}
static emptyResponse(): string {
File diff suppressed because it is too large. Load diff
+221
View File
@@ -0,0 +1,221 @@
// @ts-nocheck — fichier copié de cwmp-connector (papaye)
import { Cwmp_1_0, FaultCodeType } from './cwmp-1-0.js'
/**
* CWMP Version 1.1 Protocol Classes
*
* This version extends CWMP 1.0 with additional features:
* - Additional fault codes (9014-9019)
* - New event code: "10 AUTONOMOUS TRANSFER COMPLETE"
* - New messages: GetAllQueuedTransfers, AutonomousTransferComplete
* - AllQueuedTransferStruct and AllTransferList types
*/
export class Cwmp_1_1 extends Cwmp_1_0 {
public '@class' = '.Cwmp_1_1'
public GetAllQueuedTransfers?: GetAllQueuedTransfers
public GetAllQueuedTransfersResponse?: GetAllQueuedTransfersResponse
public AutonomousTransferComplete?: AutonomousTransferComplete
public AutonomousTransferCompleteResponse?: AutonomousTransferCompleteResponse
constructor(props: any = {}) {
super(props)
this.GetAllQueuedTransfers = props.GetAllQueuedTransfers ? new GetAllQueuedTransfers(props.GetAllQueuedTransfers) : undefined
this.GetAllQueuedTransfersResponse = props.GetAllQueuedTransfersResponse ? new GetAllQueuedTransfersResponse(props.GetAllQueuedTransfersResponse) : undefined
this.AutonomousTransferComplete = props.AutonomousTransferComplete ? new AutonomousTransferComplete(props.AutonomousTransferComplete) : undefined
this.AutonomousTransferCompleteResponse = props.AutonomousTransferCompleteResponse ? new AutonomousTransferCompleteResponse(props.AutonomousTransferCompleteResponse) : undefined
}
}
// Extended fault codes for CWMP 1.1
export enum FaultCodeType_1_1 {
// All codes from 1.0
MethodNotSupported = 9000,
RequestDenied = 9001,
InternalError = 9002,
InvalidArguments = 9003,
ResourcesExceeded = 9004,
InvalidParameterName = 9005,
InvalidParameterType = 9006,
InvalidParameterValue = 9007,
NonWritableParameter = 9008,
NotificationRequestRejected = 9009,
DownloadFailure = 9010,
UploadFailure = 9011,
FileTransferServerAuthFailure = 9012,
UnsupportedProtocolForFileTransfer = 9013,
// New codes in 1.1
DownloadFailureUnableToJoinMulticastGroup = 9014,
DownloadFailureUnableToContactFileServer = 9015,
DownloadFailureUnableToAccessFile = 9016,
DownloadFailureUnableToCompleteDownload = 9017,
DownloadFailureFileCorrupted = 9018,
DownloadFailureFileAuthenticationFailure = 9019,
// Future expansion fault codes
FutureExpansionMin = 9020,
FutureExpansionMax = 9799,
// CPE Vendor fault codes (range)
VendorFaultCodeMin = 9800,
VendorFaultCodeMax = 9899,
// ACS fault codes
ACSMethodNotSupported = 8000,
ACSRequestDenied = 8001,
ACSInternalError = 8002,
ACSInvalidArguments = 8003,
ACSResourcesExceeded = 8004,
ACSRetryRequest = 8005,
// ACS Future expansion fault codes
ACSFutureExpansionMin = 8006,
ACSFutureExpansionMax = 8799,
// ACS Vendor fault codes (range)
ACSVendorFaultCodeMin = 8800,
ACSVendorFaultCodeMax = 8899,
}
// Extended EventCode enum for CWMP 1.1
export enum EventCode_1_1 {
BOOTSTRAP = '0 BOOTSTRAP',
BOOT = '1 BOOT',
PERIODIC = '2 PERIODIC',
SCHEDULED = '3 SCHEDULED',
VALUE_CHANGE = '4 VALUE CHANGE',
KICKED = '5 KICKED',
CONNECTION_REQUEST = '6 CONNECTION REQUEST',
TRANSFER_COMPLETE = '7 TRANSFER COMPLETE',
DIAGNOSTICS_COMPLETE = '8 DIAGNOSTICS COMPLETE',
REQUEST_DOWNLOAD = '9 REQUEST DOWNLOAD',
AUTONOMOUS_TRANSFER_COMPLETE = '10 AUTONOMOUS TRANSFER COMPLETE', // New in 1.1
REBOOT = 'M Reboot',
SCHEDULE_INFORM = 'M ScheduleInform',
DOWNLOAD = 'M Download',
UPLOAD = 'M Upload',
X_METHOD = 'M X_',
X_PATTERN = 'X [0-9A-F]{6} .*',
}
export class AllQueuedTransferStruct {
public '@class' = '.AllQueuedTransferStruct'
public CommandKey: string
public State: number
public IsDownload: boolean
public FileType: string
public FileSize: number
public TargetFileName: string
constructor(props: any = {}) {
this.CommandKey = props.CommandKey || ''
this.State = props.State || 1
this.IsDownload = props.IsDownload || false
this.FileType = props.FileType || ''
this.FileSize = props.FileSize || 0
this.TargetFileName = props.TargetFileName || ''
}
}
export class AllTransferList {
public '@class' = '.AllTransferList'
public AllQueuedTransferStruct: AllQueuedTransferStruct[] = []
constructor(props: any = {}) {
this.AllQueuedTransferStruct = (props.AllQueuedTransferStruct || []).map((item: any) => new AllQueuedTransferStruct(item))
}
}
export class GetAllQueuedTransfers {
public '@class' = '.GetAllQueuedTransfers'
constructor(props: any = {}) {}
getXml() {
return `<cwmp:GetAllQueuedTransfers></cwmp:GetAllQueuedTransfers>`
}
}
export class GetAllQueuedTransfersResponse {
public '@class' = '.GetAllQueuedTransfersResponse'
public TransferList?: AllTransferList
constructor(props: any = {}) {
this.TransferList = props.TransferList ? new AllTransferList(props.TransferList) : undefined
}
getXml() {
const items = this.TransferList?.AllQueuedTransferStruct || []
const listXml = items
.map(
(t) => `
<AllQueuedTransferStruct>
<CommandKey>${t.CommandKey}</CommandKey>
<State>${t.State}</State>
<IsDownload>${t.IsDownload ? 1 : 0}</IsDownload>
<FileType>${t.FileType}</FileType>
<FileSize>${t.FileSize}</FileSize>
<TargetFileName>${t.TargetFileName}</TargetFileName>
</AllQueuedTransferStruct>`
)
.join('')
return `<cwmp:GetAllQueuedTransfersResponse>
<TransferList soap-enc:arrayType="cwmp:AllQueuedTransferStruct[${items.length}]">
${listXml}
</TransferList>
</cwmp:GetAllQueuedTransfersResponse>`
}
}
export class AutonomousTransferComplete {
public '@class' = '.AutonomousTransferComplete'
public AnnounceURL: string
public TransferURL: string
public IsDownload: boolean
public FileType: string
public FileSize: number
public TargetFileName: string
public FaultStruct: any // Reusing FaultStruct from base
public StartTime: Date
public CompleteTime: Date
constructor(props: any = {}) {
this.AnnounceURL = props.AnnounceURL || ''
this.TransferURL = props.TransferURL || ''
this.IsDownload = props.IsDownload || false
this.FileType = props.FileType || ''
this.FileSize = props.FileSize || 0
this.TargetFileName = props.TargetFileName || ''
this.FaultStruct = props.FaultStruct
this.StartTime = props.StartTime ? new Date(props.StartTime) : new Date()
this.CompleteTime = props.CompleteTime ? new Date(props.CompleteTime) : new Date()
}
getXml() {
const fault = this.FaultStruct
? `<FaultStruct><FaultCode>${this.FaultStruct.FaultCode ?? 0}</FaultCode><FaultString>${this.FaultStruct.FaultString || ''}</FaultString></FaultStruct>`
: ''
return `<cwmp:AutonomousTransferComplete>
<AnnounceURL>${this.AnnounceURL}</AnnounceURL>
<TransferURL>${this.TransferURL}</TransferURL>
<IsDownload>${this.IsDownload ? 1 : 0}</IsDownload>
<FileType>${this.FileType}</FileType>
<FileSize>${this.FileSize}</FileSize>
<TargetFileName>${this.TargetFileName}</TargetFileName>
${fault}
<StartTime>${this.StartTime.toISOString()}</StartTime>
<CompleteTime>${this.CompleteTime.toISOString()}</CompleteTime>
</cwmp:AutonomousTransferComplete>`
}
}
export class AutonomousTransferCompleteResponse {
public '@class' = '.AutonomousTransferCompleteResponse'
constructor(props: any = {}) {}
getXml() {
return '<cwmp:AutonomousTransferCompleteResponse/>'
}
}
+489
View File
@@ -0,0 +1,489 @@
// @ts-nocheck — fichier copié de cwmp-connector (papaye)
import { Cwmp_1_0, FaultStruct } from './cwmp-1-0.js'
import { Cwmp_1_1, AutonomousTransferComplete as AutonomousTransferComplete_1_1 } from './cwmp-1-1.js'
/**
* CWMP Version 1.2 Protocol Classes
*
* Extends CWMP 1.1 with:
* - ScheduleDownload / ScheduleDownloadResponse
* - CancelTransfer / CancelTransferResponse
* - ChangeDUState / ChangeDUStateResponse
* - DUStateChangeComplete / DUStateChangeCompleteResponse
* - AutonomousDUStateChangeComplete / AutonomousDUStateChangeCompleteResponse
* - TimeWindow structures and Software Module Management structures
*/
export class Cwmp_1_2 extends Cwmp_1_1 {
public '@class' = '.Cwmp_1_2'
// New RPCs
public ScheduleDownload?: ScheduleDownload
public ScheduleDownloadResponse?: ScheduleDownloadResponse
public CancelTransfer?: CancelTransfer
public CancelTransferResponse?: CancelTransferResponse
public ChangeDUState?: ChangeDUState
public ChangeDUStateResponse?: ChangeDUStateResponse
public DUStateChangeComplete?: DUStateChangeComplete
public DUStateChangeCompleteResponse?: DUStateChangeCompleteResponse
public AutonomousDUStateChangeComplete?: AutonomousDUStateChangeComplete
public AutonomousDUStateChangeCompleteResponse?: AutonomousDUStateChangeCompleteResponse
// Override AutonomousTransferComplete to use TransferCompleteFaultStruct
public AutonomousTransferComplete?: AutonomousTransferComplete
constructor(props: any = {}) {
super(props)
this.ScheduleDownload = props.ScheduleDownload ? new ScheduleDownload(props.ScheduleDownload) : undefined
this.ScheduleDownloadResponse = props.ScheduleDownloadResponse ? new ScheduleDownloadResponse(props.ScheduleDownloadResponse) : undefined
this.CancelTransfer = props.CancelTransfer ? new CancelTransfer(props.CancelTransfer) : undefined
this.CancelTransferResponse = props.CancelTransferResponse ? new CancelTransferResponse(props.CancelTransferResponse) : undefined
this.ChangeDUState = props.ChangeDUState ? new ChangeDUState(props.ChangeDUState) : undefined
this.ChangeDUStateResponse = props.ChangeDUStateResponse ? new ChangeDUStateResponse(props.ChangeDUStateResponse) : undefined
this.DUStateChangeComplete = props.DUStateChangeComplete ? new DUStateChangeComplete(props.DUStateChangeComplete) : undefined
this.DUStateChangeCompleteResponse = props.DUStateChangeCompleteResponse ? new DUStateChangeCompleteResponse(props.DUStateChangeCompleteResponse) : undefined
this.AutonomousDUStateChangeComplete = props.AutonomousDUStateChangeComplete ? new AutonomousDUStateChangeComplete(props.AutonomousDUStateChangeComplete) : undefined
this.AutonomousDUStateChangeCompleteResponse = props.AutonomousDUStateChangeCompleteResponse ? new AutonomousDUStateChangeCompleteResponse(props.AutonomousDUStateChangeCompleteResponse) : undefined
// Override from 1.1 when provided
this.AutonomousTransferComplete = props.AutonomousTransferComplete ? new AutonomousTransferComplete(props.AutonomousTransferComplete) : this.AutonomousTransferComplete
}
}
// New/extended Event Codes for 1.2
export enum EventCode_1_2 {
BOOTSTRAP = '0 BOOTSTRAP',
BOOT = '1 BOOT',
PERIODIC = '2 PERIODIC',
SCHEDULED = '3 SCHEDULED',
VALUE_CHANGE = '4 VALUE CHANGE',
KICKED = '5 KICKED',
CONNECTION_REQUEST = '6 CONNECTION REQUEST',
TRANSFER_COMPLETE = '7 TRANSFER COMPLETE',
DIAGNOSTICS_COMPLETE = '8 DIAGNOSTICS COMPLETE',
REQUEST_DOWNLOAD = '9 REQUEST DOWNLOAD',
AUTONOMOUS_TRANSFER_COMPLETE = '10 AUTONOMOUS TRANSFER COMPLETE',
DU_STATE_CHANGE_COMPLETE = '11 DU STATE CHANGE COMPLETE',
AUTONOMOUS_DU_STATE_CHANGE_COMPLETE = '12 AUTONOMOUS DU STATE CHANGE COMPLETE',
REBOOT = 'M Reboot',
SCHEDULE_INFORM = 'M ScheduleInform',
DOWNLOAD = 'M Download',
SCHEDULE_DOWNLOAD = 'M ScheduleDownload',
UPLOAD = 'M Upload',
CHANGE_DU_STATE = 'M ChangeDUState',
X_METHOD = 'M X_',
X_PATTERN = 'X [0-9A-F]{6} .*',
}
// Time window types used by ScheduleDownload
export class TimeWindowStruct {
public '@class' = '.TimeWindowStruct'
public WindowStart: number
public WindowEnd: number
public WindowMode: string
public UserMessage: string
public MaxRetries: number
constructor(props: any = {}) {
this.WindowStart = props.WindowStart ?? 0
this.WindowEnd = props.WindowEnd ?? 0
this.WindowMode = props.WindowMode ?? ''
this.UserMessage = props.UserMessage ?? ''
this.MaxRetries = props.MaxRetries ?? 0
}
}
export class TimeWindowList {
public '@class' = '.TimeWindowList'
public TimeWindowStruct: TimeWindowStruct[] = []
constructor(props: any = {}) {
this.TimeWindowStruct = (props.TimeWindowStruct || []).map((tw: any) => new TimeWindowStruct(tw))
}
}
// ScheduleDownload RPC
export class ScheduleDownload {
public '@class' = '.ScheduleDownload'
public CommandKey: string
public FileType: string
public URL: string
public Username?: string
public Password?: string
public FileSize?: number
public TargetFileName?: string
public TimeWindowList: TimeWindowList
constructor(props: any = {}) {
this.CommandKey = props.CommandKey || ''
this.FileType = props.FileType || ''
this.URL = props.URL || ''
this.Username = props.Username
this.Password = props.Password
this.FileSize = props.FileSize
this.TargetFileName = props.TargetFileName
this.TimeWindowList = new TimeWindowList(props.TimeWindowList || {})
}
getXml() {
const tw = this.TimeWindowList?.TimeWindowStruct || []
const twXml = tw
.map(
(w) => `
<TimeWindowStruct>
<WindowStart>${w.WindowStart}</WindowStart>
<WindowEnd>${w.WindowEnd}</WindowEnd>
<WindowMode>${w.WindowMode}</WindowMode>
<UserMessage>${w.UserMessage}</UserMessage>
<MaxRetries>${w.MaxRetries}</MaxRetries>
</TimeWindowStruct>`
)
.join('')
return `<cwmp:ScheduleDownload>
<CommandKey>${this.CommandKey}</CommandKey>
<FileType>${this.FileType}</FileType>
<URL>${this.URL}</URL>
<Username>${this.Username || ''}</Username>
<Password>${this.Password || ''}</Password>
<FileSize>${this.FileSize ?? 0}</FileSize>
<TargetFileName>${this.TargetFileName || ''}</TargetFileName>
<TimeWindowList soap-enc:arrayType="cwmp:TimeWindowStruct[${tw.length}]">
${twXml}
</TimeWindowList>
</cwmp:ScheduleDownload>`
}
}
export class ScheduleDownloadResponse {
public '@class' = '.ScheduleDownloadResponse'
constructor(props: any = {}) {}
getXml() {
return `<cwmp:ScheduleDownloadResponse/>`
}
}
// CancelTransfer RPC
export class CancelTransfer {
public '@class' = '.CancelTransfer'
public CommandKey: string
constructor(props: any = {}) {
this.CommandKey = props.CommandKey || ''
}
getXml() {
return `<cwmp:CancelTransfer>
<CommandKey>${this.CommandKey}</CommandKey>
</cwmp:CancelTransfer>`
}
}
export class CancelTransferResponse {
public '@class' = '.CancelTransferResponse'
constructor(props: any = {}) {}
getXml() {
return `<cwmp:CancelTransferResponse/>`
}
}
// Software Module Management types
export type DeploymentUnitState = 'Installed' | 'Uninstalled' | 'Failed'
export class OperationStruct { // abstract base
public '@class' = '.OperationStruct'
}
export class InstallOpStruct extends OperationStruct {
public '@class' = '.InstallOpStruct'
public URL: string
public UUID?: string
public Username?: string
public Password?: string
public ExecutionEnvRef?: string
constructor(props: any = {}) {
super()
this.URL = props.URL || ''
this.UUID = props.UUID
this.Username = props.Username
this.Password = props.Password
this.ExecutionEnvRef = props.ExecutionEnvRef
}
}
export class UpdateOpStruct extends OperationStruct {
public '@class' = '.UpdateOpStruct'
public UUID?: string
public Version?: string
public URL?: string
public Username?: string
public Password?: string
constructor(props: any = {}) {
super()
this.UUID = props.UUID
this.Version = props.Version
this.URL = props.URL
this.Username = props.Username
this.Password = props.Password
}
}
export class UninstallOpStruct extends OperationStruct {
public '@class' = '.UninstallOpStruct'
public UUID: string
public Version?: string
public ExecutionEnvRef?: string
constructor(props: any = {}) {
super()
this.UUID = props.UUID || ''
this.Version = props.Version
this.ExecutionEnvRef = props.ExecutionEnvRef
}
}
export class DeploymentUnitFaultStruct {
public '@class' = '.DeploymentUnitFaultStruct'
public FaultCode: number
public FaultString?: string
constructor(props: any = {}) {
this.FaultCode = props.FaultCode ?? 0
this.FaultString = props.FaultString
}
}
export class OpResultStruct {
public '@class' = '.OpResultStruct'
public UUID: string
public DeploymentUnitRef: string
public Version: string
public CurrentState: DeploymentUnitState
public Resolved: boolean
public ExecutionUnitRefList: string
public StartTime: Date
public CompleteTime: Date
public Fault: DeploymentUnitFaultStruct
constructor(props: any = {}) {
this.UUID = props.UUID || ''
this.DeploymentUnitRef = props.DeploymentUnitRef || ''
this.Version = props.Version || ''
this.CurrentState = props.CurrentState || 'Installed'
this.Resolved = !!props.Resolved
this.ExecutionUnitRefList = props.ExecutionUnitRefList || ''
this.StartTime = props.StartTime ? new Date(props.StartTime) : new Date()
this.CompleteTime = props.CompleteTime ? new Date(props.CompleteTime) : new Date()
this.Fault = new DeploymentUnitFaultStruct(props.Fault || {})
}
}
export class AutonOpResultStruct extends OpResultStruct {
public '@class' = '.AutonOpResultStruct'
public OperationPerformed: string
constructor(props: any = {}) {
super(props)
this.OperationPerformed = props.OperationPerformed || ''
}
}
// ChangeDUState RPC
export class ChangeDUState {
public '@class' = '.ChangeDUState'
public Operations: OperationStruct[] = []
public CommandKey?: string
constructor(props: any = {}) {
this.Operations = (props.Operations || []).map((op: any) => {
const type = op?.['@class'] || op?.type
if (type === 'InstallOpStruct' || op?.URL) return new InstallOpStruct(op)
if (type === 'UpdateOpStruct' || op?.Version || (op?.UUID && !op?.ExecutionEnvRef)) return new UpdateOpStruct(op)
if (type === 'UninstallOpStruct' || op?.ExecutionEnvRef || (op?.UUID && !op?.URL && !op?.Version)) return new UninstallOpStruct(op)
return new OperationStruct()
})
this.CommandKey = props.CommandKey
}
getXml() {
const opsXml = this.Operations
.map((op) => {
if (op instanceof InstallOpStruct) {
return `<Operations xsi:type="cwmp:InstallOpStruct">
<URL>${op.URL}</URL>
${op.UUID ? `<UUID>${op.UUID}</UUID>` : ''}
${op.Username ? `<Username>${op.Username}</Username>` : ''}
${op.Password ? `<Password>${op.Password}</Password>` : ''}
${op.ExecutionEnvRef ? `<ExecutionEnvRef>${op.ExecutionEnvRef}</ExecutionEnvRef>` : ''}
</Operations>`
}
if (op instanceof UpdateOpStruct) {
return `<Operations xsi:type="cwmp:UpdateOpStruct">
${op.UUID ? `<UUID>${op.UUID}</UUID>` : ''}
${op.Version ? `<Version>${op.Version}</Version>` : ''}
${op.URL ? `<URL>${op.URL}</URL>` : ''}
${op.Username ? `<Username>${op.Username}</Username>` : ''}
${op.Password ? `<Password>${op.Password}</Password>` : ''}
</Operations>`
}
if (op instanceof UninstallOpStruct) {
return `<Operations xsi:type="cwmp:UninstallOpStruct">
<UUID>${op.UUID}</UUID>
${op.Version ? `<Version>${op.Version}</Version>` : ''}
${op.ExecutionEnvRef ? `<ExecutionEnvRef>${op.ExecutionEnvRef}</ExecutionEnvRef>` : ''}
</Operations>`
}
return ''
})
.join('')
return `<cwmp:ChangeDUState>
${opsXml}
${this.CommandKey ? `<CommandKey>${this.CommandKey}</CommandKey>` : ''}
</cwmp:ChangeDUState>`
}
}
export class ChangeDUStateResponse {
public '@class' = '.ChangeDUStateResponse'
constructor(props: any = {}) {}
getXml() {
return `<cwmp:ChangeDUStateResponse/>`
}
}
// DUStateChangeComplete RPCs
export class DUStateChangeComplete {
public '@class' = '.DUStateChangeComplete'
public Results: OpResultStruct[]
public CommandKey?: string
constructor(props: any = {}) {
this.Results = (props.Results || []).map((r: any) => new OpResultStruct(r))
this.CommandKey = props.CommandKey
}
getXml() {
const resultsXml = this.Results
.map(
(r) => `
<Results>
<UUID>${r.UUID}</UUID>
<DeploymentUnitRef>${r.DeploymentUnitRef}</DeploymentUnitRef>
<Version>${r.Version}</Version>
<CurrentState>${r.CurrentState}</CurrentState>
<Resolved>${r.Resolved ? 1 : 0}</Resolved>
<ExecutionUnitRefList>${r.ExecutionUnitRefList}</ExecutionUnitRefList>
<StartTime>${r.StartTime.toISOString()}</StartTime>
<CompleteTime>${r.CompleteTime.toISOString()}</CompleteTime>
<Fault>
<FaultCode>${r.Fault.FaultCode}</FaultCode>
${r.Fault.FaultString ? `<FaultString>${r.Fault.FaultString}</FaultString>` : ''}
</Fault>
</Results>`
)
.join('')
return `<cwmp:DUStateChangeComplete>
${resultsXml}
${this.CommandKey ? `<CommandKey>${this.CommandKey}</CommandKey>` : ''}
</cwmp:DUStateChangeComplete>`
}
}
export class DUStateChangeCompleteResponse {
public '@class' = '.DUStateChangeCompleteResponse'
constructor(props: any = {}) {}
getXml() {
return `<cwmp:DUStateChangeCompleteResponse/>`
}
}
export class AutonomousDUStateChangeComplete {
public '@class' = '.AutonomousDUStateChangeComplete'
public Results: AutonOpResultStruct[]
constructor(props: any = {}) {
this.Results = (props.Results || []).map((r: any) => new AutonOpResultStruct(r))
}
getXml() {
const resultsXml = this.Results
.map(
(r) => `
<Results>
<UUID>${r.UUID}</UUID>
<DeploymentUnitRef>${r.DeploymentUnitRef}</DeploymentUnitRef>
<Version>${r.Version}</Version>
<CurrentState>${r.CurrentState}</CurrentState>
<Resolved>${r.Resolved ? 1 : 0}</Resolved>
<ExecutionUnitRefList>${r.ExecutionUnitRefList}</ExecutionUnitRefList>
<StartTime>${r.StartTime.toISOString()}</StartTime>
<CompleteTime>${r.CompleteTime.toISOString()}</CompleteTime>
<Fault>
<FaultCode>${r.Fault.FaultCode}</FaultCode>
${r.Fault.FaultString ? `<FaultString>${r.Fault.FaultString}</FaultString>` : ''}
</Fault>
<OperationPerformed>${r.OperationPerformed}</OperationPerformed>
</Results>`
)
.join('')
return `<cwmp:AutonomousDUStateChangeComplete>
${resultsXml}
</cwmp:AutonomousDUStateChangeComplete>`
}
}
export class AutonomousDUStateChangeCompleteResponse {
public '@class' = '.AutonomousDUStateChangeCompleteResponse'
constructor(props: any = {}) {}
getXml() {
return `<cwmp:AutonomousDUStateChangeCompleteResponse/>`
}
}
// TransferComplete-specific fault struct for 1.2
export class TransferCompleteFaultStruct {
public '@class' = '.TransferCompleteFaultStruct'
public FaultCode: number
public FaultString: string
constructor(props: any = {}) {
this.FaultCode = props.FaultCode ?? 0
this.FaultString = props.FaultString || ''
}
}
// Override of 1.1 AutonomousTransferComplete to use TransferCompleteFaultStruct
export class AutonomousTransferComplete extends AutonomousTransferComplete_1_1 {
public '@class' = '.AutonomousTransferComplete'
public FaultStruct: TransferCompleteFaultStruct
constructor(props: any = {}) {
super(props)
this.FaultStruct = new TransferCompleteFaultStruct(props.FaultStruct || {})
}
getXml() {
return `<cwmp:AutonomousTransferComplete>
<AnnounceURL>${this.AnnounceURL}</AnnounceURL>
<TransferURL>${this.TransferURL}</TransferURL>
<IsDownload>${this.IsDownload ? 1 : 0}</IsDownload>
<FileType>${this.FileType}</FileType>
<FileSize>${this.FileSize}</FileSize>
<TargetFileName>${this.TargetFileName}</TargetFileName>
<FaultStruct>
<FaultCode>${this.FaultStruct.FaultCode}</FaultCode>
<FaultString>${this.FaultStruct.FaultString}</FaultString>
</FaultStruct>
<StartTime>${this.StartTime.toISOString()}</StartTime>
<CompleteTime>${this.CompleteTime.toISOString()}</CompleteTime>
</cwmp:AutonomousTransferComplete>`
}
}
+30
View File
@@ -0,0 +1,30 @@
// @ts-nocheck — fichier copié de cwmp-connector (papaye)
import { Cwmp_1_2 } from './cwmp-1-2.js'
/**
* CWMP Version 1.3 Protocol Classes
*
* Extends CWMP 1.2 with:
* - New SOAP Header element: SessionTimeout
*/
export class Cwmp_1_3 extends Cwmp_1_2 {
public '@class' = '.Cwmp_1_3'
public SessionTimeout?: SessionTimeout
constructor(props: any = {}) {
super(props)
this.SessionTimeout = props.SessionTimeout ? new SessionTimeout(props.SessionTimeout) : undefined
}
}
// SOAP Header element for CWMP 1.3
export class SessionTimeout {
public '@class' = '.SessionTimeout'
public MustUnderstand: string = '0'
public _: number
constructor(props: any = {}) {
this.MustUnderstand = props.MustUnderstand ?? '0'
this._ = props._ ?? 0
}
}
+69
View File
@@ -0,0 +1,69 @@
// @ts-nocheck — fichier copié de cwmp-connector (papaye)
import { Cwmp_1_3 } from './cwmp-1-3.js'
/**
* CWMP Version 1.4 Protocol Classes
*
* Extends CWMP 1.3 with:
* - New ACS Fault Code 8006 (handled at validation level, not needed here)
* - New Event Code: 13 WAKEUP
* - New SOAP Header elements: SupportedCWMPVersions, UseCWMPVersion
*/
export class Cwmp_1_4 extends Cwmp_1_3 {
public '@class' = '.Cwmp_1_4'
public SupportedCWMPVersions?: SupportedCWMPVersions
public UseCWMPVersion?: UseCWMPVersion
constructor(props: any = {}) {
super(props)
this.SupportedCWMPVersions = props.SupportedCWMPVersions ? new SupportedCWMPVersions(props.SupportedCWMPVersions) : undefined
this.UseCWMPVersion = props.UseCWMPVersion ? new UseCWMPVersion(props.UseCWMPVersion) : undefined
}
}
export enum EventCode_1_4 {
BOOTSTRAP = '0 BOOTSTRAP',
BOOT = '1 BOOT',
PERIODIC = '2 PERIODIC',
SCHEDULED = '3 SCHEDULED',
VALUE_CHANGE = '4 VALUE CHANGE',
KICKED = '5 KICKED',
CONNECTION_REQUEST = '6 CONNECTION REQUEST',
TRANSFER_COMPLETE = '7 TRANSFER COMPLETE',
DIAGNOSTICS_COMPLETE = '8 DIAGNOSTICS COMPLETE',
REQUEST_DOWNLOAD = '9 REQUEST DOWNLOAD',
AUTONOMOUS_TRANSFER_COMPLETE = '10 AUTONOMOUS TRANSFER COMPLETE',
DU_STATE_CHANGE_COMPLETE = '11 DU STATE CHANGE COMPLETE',
AUTONOMOUS_DU_STATE_CHANGE_COMPLETE = '12 AUTONOMOUS DU STATE CHANGE COMPLETE',
WAKEUP = '13 WAKEUP',
REBOOT = 'M Reboot',
SCHEDULE_INFORM = 'M ScheduleInform',
DOWNLOAD = 'M Download',
SCHEDULE_DOWNLOAD = 'M ScheduleDownload',
UPLOAD = 'M Upload',
CHANGE_DU_STATE = 'M ChangeDUState',
X_METHOD = 'M X_',
X_PATTERN = 'X [0-9A-F]{6} .*',
}
export class SupportedCWMPVersions {
public '@class' = '.SupportedCWMPVersions'
public MustUnderstand?: string // should be 0 per spec, optional due to SOAP 1.1 quirks
public _: string // space or comma separated list per implementation
constructor(props: any = {}) {
this.MustUnderstand = props.MustUnderstand ?? '0'
this._ = props._ || ''
}
}
export class UseCWMPVersion {
public '@class' = '.UseCWMPVersion'
public MustUnderstand: string = '1'
public _: string
constructor(props: any = {}) {
this.MustUnderstand = props.MustUnderstand ?? '1'
this._ = props._ || ''
}
}
+47 -27
View File
@@ -1,4 +1,7 @@
import { XMLParser } from 'fast-xml-parser'
import type { CwmpMessage, SoapEnvelope, SoapHeader } from './tr069_types.js'
export { type SoapEnvelope, type SoapHeader, type CwmpMessage } from './tr069_types.js'
export const CWMP_VERSIONS: Record<string, string> = {
'urn:dslforum-org:cwmp-1-0': '1.0',
@@ -19,30 +22,25 @@ const xmlParser = new XMLParser({
},
})
export interface SoapEnvelope {
header: SoapHeader
body: any
cwmpVersion: string
}
export interface SoapHeader {
id?: string
holdRequests?: boolean
noMoreRequests?: boolean
}
export interface CwmpMessage {
type: string
data: any
}
export function parseSoapXml(xml: string): SoapEnvelope {
const parsed = xmlParser.parse(xml)
const envelope = parsed['soap-env:Envelope'] || parsed['SOAP-ENV:Envelope'] || parsed['soapenv:Envelope'] || {}
const header = envelope['soap-env:Header'] || envelope['SOAP-ENV:Header'] || envelope['soapenv:Header'] || {}
const body = envelope['soap-env:Body'] || envelope['SOAP-ENV:Body'] || envelope['soapenv:Body'] || {}
const parsed = xmlParser.parse(xml) as Record<string, unknown>
const envelope =
(parsed['soap-env:Envelope'] as Record<string, unknown>) ||
(parsed['SOAP-ENV:Envelope'] as Record<string, unknown>) ||
(parsed['soapenv:Envelope'] as Record<string, unknown>) ||
{}
const header =
(envelope['soap-env:Header'] as Record<string, unknown>) ||
(envelope['SOAP-ENV:Header'] as Record<string, unknown>) ||
(envelope['soapenv:Header'] as Record<string, unknown>) ||
{}
const body =
(envelope['soap-env:Body'] as Record<string, unknown>) ||
(envelope['SOAP-ENV:Body'] as Record<string, unknown>) ||
(envelope['soapenv:Body'] as Record<string, unknown>) ||
{}
// Detect CWMP version from namespace
// Detect CWMP version from namespace attributes
let cwmpVersion = '1.0'
for (const [, value] of Object.entries(envelope)) {
if (typeof value === 'string' && value.startsWith('urn:dslforum-org:cwmp-')) {
@@ -54,26 +52,48 @@ export function parseSoapXml(xml: string): SoapEnvelope {
const soapHeader: SoapHeader = {}
const idNode = header['cwmp:ID'] || header['cwmp:Id']
if (idNode) {
soapHeader.id = typeof idNode === 'string' ? idNode : idNode._ || idNode
soapHeader.id =
typeof idNode === 'string'
? idNode
: (idNode as { _?: string })?._ || String(idNode)
}
return { header: soapHeader, body, cwmpVersion }
}
export function extractCwmpMessage(body: any): CwmpMessage | null {
/** Types de méthodes CWMP acceptés en entrée (CPE→ACS) */
const ALLOWED_CWMP_TYPES = new Set([
'Inform',
'TransferComplete',
'GetParameterNamesResponse',
'GetParameterValuesResponse',
'GetParameterAttributesResponse',
'SetParameterValuesResponse',
'SetParameterAttributesResponse',
'RebootResponse',
'FactoryResetResponse',
'DownloadResponse',
'UploadResponse',
'ScheduleInformResponse',
'Fault',
])
export function extractCwmpMessage(body: Record<string, unknown>): CwmpMessage | null {
for (const key of Object.keys(body)) {
if (key.startsWith('cwmp:')) {
const type = key.replace('cwmp:', '')
return { type, data: body[key] }
if (!ALLOWED_CWMP_TYPES.has(type)) return null
const data = body[key]
return { type, data } as CwmpMessage
}
}
// Detect SOAP Fault (CPE sends soap-env:Fault / SOAP-ENV:Fault when it cannot process a request)
// SOAP Fault envoyé par le CPE (soap-env:Fault / SOAP-ENV:Fault)
const faultKey = Object.keys(body).find(
(k) => k.toLowerCase().endsWith(':fault') || k === 'Fault'
)
if (faultKey) {
return { type: 'Fault', data: body[faultKey] }
return { type: 'Fault', data: body[faultKey] } as CwmpMessage
}
return null
+199
View File
@@ -0,0 +1,199 @@
/**
* Classes SOAP Envelope (extraites de cwmp-connector/src/protocol/soap.ts)
*
* Séparées de soap.ts (le module de parsing d'adam-acs) pour éviter les conflits de nom.
*/
import { Cwmp_1_0 } from './cwmp-1-0.js'
import { Cwmp_1_1 } from './cwmp-1-1.js'
import { Cwmp_1_2 } from './cwmp-1-2.js'
import { Cwmp_1_3, SessionTimeout } from './cwmp-1-3.js'
import { Cwmp_1_4, SupportedCWMPVersions, UseCWMPVersion } from './cwmp-1-4.js'
type NamespaceDescriptor = {
SoapEnc: string
SoapEnv: string
Xsd: string
Xsi: string
Cwmp: string
}
export const NAMESPACES: Record<string, NamespaceDescriptor> = {
'1.0': {
SoapEnc: 'http://schemas.xmlsoap.org/soap/encoding/',
SoapEnv: 'http://schemas.xmlsoap.org/soap/envelope/',
Xsd: 'http://www.w3.org/2001/XMLSchema',
Xsi: 'http://www.w3.org/2001/XMLSchema-instance',
Cwmp: 'urn:dslforum-org:cwmp-1-0',
},
'1.1': {
SoapEnc: 'http://schemas.xmlsoap.org/soap/encoding/',
SoapEnv: 'http://schemas.xmlsoap.org/soap/envelope/',
Xsd: 'http://www.w3.org/2001/XMLSchema',
Xsi: 'http://www.w3.org/2001/XMLSchema-instance',
Cwmp: 'urn:dslforum-org:cwmp-1-1',
},
'1.2': {
SoapEnc: 'http://schemas.xmlsoap.org/soap/encoding/',
SoapEnv: 'http://schemas.xmlsoap.org/soap/envelope/',
Xsd: 'http://www.w3.org/2001/XMLSchema',
Xsi: 'http://www.w3.org/2001/XMLSchema-instance',
Cwmp: 'urn:dslforum-org:cwmp-1-2',
},
'1.3': {
SoapEnc: 'http://schemas.xmlsoap.org/soap/encoding/',
SoapEnv: 'http://schemas.xmlsoap.org/soap/envelope/',
Xsd: 'http://www.w3.org/2001/XMLSchema',
Xsi: 'http://www.w3.org/2001/XMLSchema-instance',
Cwmp: 'urn:dslforum-org:cwmp-1-3',
},
'1.4': {
SoapEnc: 'http://schemas.xmlsoap.org/soap/encoding/',
SoapEnv: 'http://schemas.xmlsoap.org/soap/envelope/',
Xsd: 'http://www.w3.org/2001/XMLSchema',
Xsi: 'http://www.w3.org/2001/XMLSchema-instance',
Cwmp: 'urn:dslforum-org:cwmp-1-4',
},
}
export class SoapEnvelopeAttribute {
public SoapEnc: string
public SoapEnv: string
public Xsd: string
public Xsi: string
public Cwmp: string
constructor(props: NamespaceDescriptor | Record<string, string> = NAMESPACES['1.0']) {
this.SoapEnc = props?.SoapEnc || 'http://schemas.xmlsoap.org/soap/encoding/'
this.SoapEnv = props?.SoapEnv || 'http://schemas.xmlsoap.org/soap/envelope/'
this.Xsd = props?.Xsd || 'http://www.w3.org/2001/XMLSchema'
this.Xsi = props?.Xsi || 'http://www.w3.org/2001/XMLSchema-instance'
this.Cwmp = props?.Cwmp || 'urn:dslforum-org:cwmp-1-0'
}
public isEqualTo(namespace: NamespaceDescriptor): boolean {
return (
this.SoapEnc === namespace.SoapEnc &&
this.SoapEnv === namespace.SoapEnv &&
this.Xsd === namespace.Xsd &&
this.Xsi === namespace.Xsi &&
this.Cwmp === namespace.Cwmp
)
}
}
export class SoapCwmpIdAttribute {
MustUnderstand: string
constructor(props: { MustUnderstand?: string } = {}) {
this.MustUnderstand = props.MustUnderstand || '1'
}
getXml(): string {
return ` soap-env:mustUnderstand="${this.MustUnderstand}"`
}
}
export class CwmpID {
$?: SoapCwmpIdAttribute
_?: string
constructor(props: { $?: { MustUnderstand?: string }; _?: string } = {}) {
this.$ = props?.$ ? new SoapCwmpIdAttribute(props.$) : undefined
this._ = props._ || ''
}
getXml(): string {
return `<cwmp:ID${this.$?.getXml() || ''}>${this._}</cwmp:ID>`
}
}
export type CwmpBody = Cwmp_1_0 | Cwmp_1_1 | Cwmp_1_2 | Cwmp_1_3 | Cwmp_1_4
export class SoapEnvelopeHeader {
ID: CwmpID
NoMoreRequests: number
SessionTimeout?: SessionTimeout
SupportedCWMPVersions?: SupportedCWMPVersions
UseCWMPVersion?: UseCWMPVersion
constructor(props: Record<string, unknown> = {}) {
this.ID = new CwmpID(props?.ID as any)
this.NoMoreRequests = (props?.NoMoreRequests as number) || 0
this.SessionTimeout = props?.SessionTimeout ? new SessionTimeout(props.SessionTimeout) : undefined
this.SupportedCWMPVersions = props?.SupportedCWMPVersions
? new SupportedCWMPVersions(props.SupportedCWMPVersions)
: undefined
this.UseCWMPVersion = props?.UseCWMPVersion ? new UseCWMPVersion(props.UseCWMPVersion) : undefined
}
}
export class SoapEnvelope {
$?: SoapEnvelopeAttribute
Header: SoapEnvelopeHeader
Body: CwmpBody
version: '1.0' | '1.1' | '1.2' | '1.3' | '1.4'
constructor(props: Record<string, unknown> = {}) {
this.version = (props?.version as '1.0' | '1.1' | '1.2' | '1.3' | '1.4') || '1.0'
this.$ = props?.$ ? new SoapEnvelopeAttribute(props.$ as any) : new SoapEnvelopeAttribute(NAMESPACES[this.version])
this.Header = new SoapEnvelopeHeader((props.Header as Record<string, unknown>) || {})
const bodyProps = (props.Body as Record<string, unknown>) || {}
switch (this.version) {
case '1.4': this.Body = new Cwmp_1_4(bodyProps); break
case '1.3': this.Body = new Cwmp_1_3(bodyProps); break
case '1.2': this.Body = new Cwmp_1_2(bodyProps); break
case '1.1': this.Body = new Cwmp_1_1(bodyProps); break
default: this.Body = new Cwmp_1_0(bodyProps); break
}
}
getXml(): string {
const headerParts: string[] = []
if (this.Header.ID) headerParts.push(this.Header.ID.getXml())
if (this.Header.SessionTimeout) {
headerParts.push(
`<cwmp:SessionTimeout soap-env:mustUnderstand="${this.Header.SessionTimeout.MustUnderstand}">${this.Header.SessionTimeout._}</cwmp:SessionTimeout>`
)
}
if (this.Header.SupportedCWMPVersions) {
headerParts.push(
`<cwmp:SupportedCWMPVersions${this.Header.SupportedCWMPVersions.MustUnderstand !== undefined ? ` soap-env:mustUnderstand="${this.Header.SupportedCWMPVersions.MustUnderstand}"` : ''}>${this.Header.SupportedCWMPVersions._}</cwmp:SupportedCWMPVersions>`
)
}
if (this.Header.UseCWMPVersion) {
headerParts.push(
`<cwmp:UseCWMPVersion soap-env:mustUnderstand="${this.Header.UseCWMPVersion.MustUnderstand}">${this.Header.UseCWMPVersion._}</cwmp:UseCWMPVersion>`
)
}
// noMoreRequests est un header CPE→ACS uniquement (TR-069 §3.7.1.4) — ne pas l'émettre côté ACS
return `<soap-env:Envelope
xmlns:soap-enc="${this.$?.SoapEnc}"
xmlns:soap-env="${this.$?.SoapEnv}"
xmlns:xsd="${this.$?.Xsd}"
xmlns:xsi="${this.$?.Xsi}"
xmlns:cwmp="${this.$?.Cwmp}">
<soap-env:Header>
${headerParts.join('\n ')}
</soap-env:Header>
<soap-env:Body>
${
this.Body.InformResponse?.getXml() ||
this.Body.GetParameterValues?.getXml() ||
this.Body.GetParameterNames?.getXml() ||
this.Body.GetParameterAttributes?.getXml() ||
this.Body.SetParameterValues?.getXml() ||
this.Body.SetParameterAttributes?.getXml() ||
this.Body.ScheduleInform?.getXml() ||
this.Body.Download?.getXml() ||
this.Body.Upload?.getXml() ||
this.Body.Reboot?.getXml() ||
this.Body.FactoryReset?.getXml() ||
this.Body.TransferCompleteResponse?.getXml() ||
''
}
</soap-env:Body>
</soap-env:Envelope>`
}
}
+205
View File
@@ -0,0 +1,205 @@
/**
* TR-069 / CWMP Protocol Types
*
* Réexporte les classes et types des fichiers cwmp-1-x copiés depuis cwmp-connector (papaye).
* Point d'entrée unique pour tous les types CWMP dans adam-acs.
*/
// ─── Classes & enums de base (CWMP 1.0) ──────────────────────────────────────
export {
Cwmp_1_0,
EventCode,
FaultCodeType,
// Structures parsées CPE → ACS
Inform,
InformResponse,
TransferComplete,
TransferCompleteResponse,
// Structures ACS → CPE (réponses)
GetParameterNamesResponse,
GetParameterValuesResponse,
GetParameterAttributesResponse,
SetParameterValuesResponse,
SetParameterAttributesResponse,
RebootResponse,
FactoryResetResponse,
DownloadResponse,
UploadResponse,
ScheduleInformResponse,
// Structures de building block
DeviceIdStruct,
EventStruct,
Event,
ParameterValueStruct,
ParameterValueList,
ParameterValueAttribute,
ValueObject,
ValueObjectAttribute,
ParameterInfoStruct,
ParameterInfoList,
ParameterAttributeStruct,
ParameterAttributeList,
SetParameterAttributesStruct,
SetParameterAttributesList,
AccessList,
FaultStruct,
FaultDetail,
Fault,
// Commandes ACS → CPE
GetParameterNames,
GetParameterValues,
GetParameterAttributes,
SetParameterValues,
SetParameterAttributes,
Download,
Upload,
Reboot,
FactoryReset,
ScheduleInform,
// Types primitifs
type CommandKeyType,
type ObjectNameType,
} from './cwmp-1-0.js'
// ─── CWMP 1.1 ─────────────────────────────────────────────────────────────────
export { Cwmp_1_1, EventCode_1_1, FaultCodeType_1_1 } from './cwmp-1-1.js'
// ─── CWMP 1.2 ─────────────────────────────────────────────────────────────────
export { Cwmp_1_2, EventCode_1_2 } from './cwmp-1-2.js'
// ─── CWMP 1.3 ─────────────────────────────────────────────────────────────────
export { Cwmp_1_3, SessionTimeout } from './cwmp-1-3.js'
// ─── CWMP 1.4 ─────────────────────────────────────────────────────────────────
export { Cwmp_1_4, EventCode_1_4, SupportedCWMPVersions, UseCWMPVersion } from './cwmp-1-4.js'
// ─── Types locaux complémentaires ─────────────────────────────────────────────
import type { Cwmp_1_0 } from './cwmp-1-0.js'
import type { Cwmp_1_1 } from './cwmp-1-1.js'
import type { Cwmp_1_2 } from './cwmp-1-2.js'
import type { Cwmp_1_3 } from './cwmp-1-3.js'
import type { Cwmp_1_4 } from './cwmp-1-4.js'
import type {
Inform,
TransferComplete,
GetParameterNamesResponse,
GetParameterValuesResponse,
GetParameterAttributesResponse,
SetParameterValuesResponse,
Fault as SoapFaultBody,
} from './cwmp-1-0.js'
/** Union de tous les corps CWMP selon la version du protocole */
export type CwmpBody = Cwmp_1_0 | Cwmp_1_1 | Cwmp_1_2 | Cwmp_1_3 | Cwmp_1_4
/** Union discriminée des messages CWMP parsés depuis le XML CPE */
export type CwmpMessage =
| { type: 'Inform'; data: Inform }
| { type: 'TransferComplete'; data: TransferComplete }
| { type: 'GetParameterNamesResponse'; data: GetParameterNamesResponse }
| { type: 'GetParameterValuesResponse'; data: GetParameterValuesResponse }
| { type: 'GetParameterAttributesResponse'; data: GetParameterAttributesResponse }
| { type: 'SetParameterValuesResponse'; data: SetParameterValuesResponse }
| { type: 'SetParameterAttributesResponse'; data: Record<string, unknown> }
| { type: 'RebootResponse'; data: Record<string, unknown> }
| { type: 'FactoryResetResponse'; data: Record<string, unknown> }
| { type: 'DownloadResponse'; data: Record<string, unknown> }
| { type: 'UploadResponse'; data: Record<string, unknown> }
| { type: 'ScheduleInformResponse'; data: Record<string, unknown> }
| { type: 'Fault'; data: SoapFaultBody }
/** Header SOAP simplifié (extrait par le parser) */
export interface SoapHeader {
id?: string
holdRequests?: boolean
noMoreRequests?: boolean
}
/** Enveloppe SOAP telle que retournée par parseSoapXml() */
export interface SoapEnvelope {
header: SoapHeader
body: Record<string, unknown>
cwmpVersion: string
}
// ─── Types des payloads de tâches CWMP ────────────────────────────────────────
export type CWMPTaskType =
| 'get_parameter_names'
| 'get_parameter_attributes'
| 'get_parameter_values'
| 'set_parameter_values'
| 'set_parameter_attributes'
| 'schedule_inform'
| 'download'
| 'upload'
| 'factory_reset'
| 'reboot'
export interface SetParameterValuesTaskPayload {
parameterValueList: Array<{
Name: string
Value: { _: string | number | boolean; $?: { Type: string } }
}>
parameterKey?: string
}
export interface SetParameterAttributesTaskPayload {
name: string
notificationChange: boolean
notification: number
accessListChange: boolean
accessList: string[]
}
export interface ScheduleInformTaskPayload {
delayInSeconds: number
commandKey?: string
}
export interface DownloadTaskPayload {
commandKey?: string
fileType?: string
url: string
username?: string
password?: string
fileSize?: number
targetFileName?: string
delaySeconds?: number
successUrl?: string
failureUrl?: string
}
export interface UploadTaskPayload {
commandKey?: string
fileType?: string
url: string
username?: string
password?: string
delaySeconds?: number
}
export interface RebootTaskPayload {
commandKey?: string
}
export type CWMPTaskPayload =
| string // get_parameter_names
| string[] // get_parameter_values / get_parameter_attributes
| SetParameterValuesTaskPayload
| SetParameterAttributesTaskPayload[]
| ScheduleInformTaskPayload
| DownloadTaskPayload
| UploadTaskPayload
| RebootTaskPayload
| Record<string, never> // factory_reset
/** Données de résultat retournées dans TaskResult.data selon le type de tâche */
export type CWMPTaskResultData =
| GetParameterNamesResponse
| GetParameterValuesResponse
| GetParameterAttributesResponse
| SetParameterValuesResponse
| SoapFaultBody
| Record<string, unknown>
@@ -95,13 +95,14 @@ export class ConfigurationEvaluationState {
if (this.awaitingPhase === 'gpn_response') {
this.awaitingPhase = null
if (context.message?.success && context.message.data) {
const infoList = context.message.data?.ParameterList?.ParameterInfoStruct || []
const gpnResp = context.message.data as import('../../../app/protocol/tr069_types.js').GetParameterNamesResponse
const infoList = gpnResp?.ParameterList?.ParameterInfoStruct ?? []
const leafNames: string[] = infoList
.map((info: any) => {
.map((info) => {
const n = info?.Name
return typeof n === 'string' ? n : (n && typeof n._ === 'string' ? n._ : null)
return typeof n === 'string' ? n : (n && typeof (n as { _?: string })._ === 'string' ? (n as { _: string })._ : null)
})
.filter((name: any): name is string => !!name && !name.endsWith('.'))
.filter((name): name is string => !!name && !name.endsWith('.'))
if (leafNames.length > 0) {
this.pendingValuesQueue.push(...leafNames)
}
@@ -115,11 +116,12 @@ export class ConfigurationEvaluationState {
} else if (this.awaitingPhase === 'gpv_response') {
this.awaitingPhase = null
if (context.message?.success && context.message.data) {
const list = context.message.data?.ParameterList?.ParameterValueStruct
if (Array.isArray(list) && list.length > 0) {
const normalized = list.map((pv: any) => ({
const gpvResp = context.message.data as import('../../../app/protocol/tr069_types.js').GetParameterValuesResponse
const list = gpvResp?.ParameterList?.ParameterValueStruct ?? []
if (list.length > 0) {
const normalized = list.map((pv) => ({
...pv,
Name: typeof pv?.Name === 'string' ? pv.Name : pv?.Name?._ || pv?.Name,
Name: typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || pv?.Name,
}))
context.device.updateFromValues(normalized)
logger.debug({ identifier: context.device.identifier, count: normalized.length }, '[ConfigEval] Inventory GPV values saved to device')
+51 -40
View File
@@ -4,15 +4,24 @@ 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'
// Parameter types (simplified from legacy provisioning domain)
interface SetParameterValue {
// ─── Action parameter interfaces (exported pour usage externe) ─────────────────
export interface SetParameterValue {
parameterName: string
value: string | number | boolean
type?: string
}
interface SetParameterAttribute {
export interface SetParameterAttribute {
name: string
notificationChange: boolean
notification: number
@@ -20,12 +29,12 @@ interface SetParameterAttribute {
accessList: string[]
}
interface SetScheduleInform {
export interface SetScheduleInform {
delayInSeconds: number
commandKey?: string
}
interface DownloadParameter {
export interface DownloadParameter {
commandKey: string
fileType: string
url: string
@@ -38,7 +47,7 @@ interface DownloadParameter {
failureUrl?: string
}
interface UploadParameter {
export interface UploadParameter {
commandKey: string
fileType: string
url: string
@@ -47,11 +56,11 @@ interface UploadParameter {
delayInSeconds?: number
}
interface FactoryResetParameter {
export interface FactoryResetParameter {
commandKey?: string
}
interface RebootParameter {
export interface RebootParameter {
commandKey?: string
}
@@ -94,9 +103,9 @@ export class DeviceActionExecutor {
const wrappedCallback = async (result: TaskResult) => {
if (result.status === ResultStatus.Success) {
try {
const resp: any = result.data
const list = resp?.ParameterList?.ParameterValueStruct
if (Array.isArray(list) && list.length > 0) {
const resp = result.data as GetParameterValuesResponse
const list: ParameterValueStruct[] = resp?.ParameterList?.ParameterValueStruct ?? []
if (list.length > 0) {
device.updateFromValues(list)
await this.persistDevice(device)
}
@@ -118,13 +127,15 @@ export class DeviceActionExecutor {
const wrappedCallback = async (result: TaskResult) => {
if (result.status === ResultStatus.Success) {
try {
const resp: any = result.data
const list = resp?.ParameterList?.ParameterAttributeStruct
if (Array.isArray(list) && list.length > 0) {
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 = attr?.Name
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 notification: number | undefined =
typeof attr?.Notification === 'number' ? attr.Notification : undefined
const accessList: string[] | undefined = attr?.AccessList?.string
const param = device.getParameter(name)
@@ -157,7 +168,9 @@ export class DeviceActionExecutor {
const wrappedCallback = async (result: TaskResult) => {
if (result.status === ResultStatus.Success) {
try {
device.organizeParameters(result.data as any, new Date())
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()
@@ -167,24 +180,23 @@ export class DeviceActionExecutor {
}
} 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 =
result.data?.FaultCode ??
result.data?.faultCode ??
result.data?.Detail?.Fault?.FaultCode ??
result.data?.detail?.Fault?.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 (GPN_UNSUPPORTED_CODES.includes(code)) {
if (code !== undefined && GPN_UNSUPPORTED_CODES.includes(code)) {
device.setGpnStatus('unsupported')
device.recordGpnFailure()
await this.persistDevice(device)
} else if (GPN_TRANSIENT_CODES.includes(code)) {
// Transient error: log warning, do not mark unsupported, allow retry this session
} else if (code !== undefined && GPN_TRANSIENT_CODES.includes(code)) {
logger.warn({ code, deviceId: device.deviceId?.serialNumber }, 'GPN transient error (9002) — will not mark unsupported')
}
// Network timeout or other codes: do not mark unsupported, skip GPN for this session only
}
return await callback(result)
}
@@ -199,7 +211,7 @@ export class DeviceActionExecutor {
): Promise<string> {
const existingParameters = device.getParameters()
const inferType = (val: any): string => {
const inferType = (val: string | number | boolean): string => {
const t = typeof val
switch (t) {
case 'boolean': return 'xsd:boolean'
@@ -218,18 +230,17 @@ export class DeviceActionExecutor {
const pvStructs = parameters.map((p) => {
const existing = existingParameters.find((ep) => ep.name === p.parameterName)
// Priority: 1) explicit type from config, 2) known device type, 3) inferred from JS value
let xsdType: string
if (p.type && cwmpTypeMap[p.type]) {
xsdType = cwmpTypeMap[p.type]
} else if (existing?.type && existing.type !== ParameterType.UNKNOWN) {
xsdType = existing.type // already in xsd: format (e.g. 'xsd:string')
xsdType = existing.type
} else {
xsdType = inferType(p.value)
}
return {
Name: p.parameterName,
Value: { _: p.value, $: { Type: xsdType } },
Value: { _: p.value as string, $: { Type: xsdType } },
}
})
@@ -241,7 +252,7 @@ export class DeviceActionExecutor {
{ parameterValueList: pvStructs, parameterKey },
async (result: TaskResult) => {
if (result.status === ResultStatus.Success) {
device.updateFromValues(pvStructs as any)
device.updateFromValues(pvStructs as ParameterValueStruct[])
await this.persistDevice(device)
return await callback(true)
}
@@ -260,7 +271,7 @@ export class DeviceActionExecutor {
notificationChange: p.notificationChange ?? false,
notification: p.notification ?? 0,
accessListChange: p.accessListChange ?? false,
accessList: (p.accessList as any)?.string || [],
accessList: (p.accessList as unknown as { string?: string[] })?.string || p.accessList || [],
}))
return this.taskQueue.addTask(
@@ -300,7 +311,7 @@ export class DeviceActionExecutor {
return this.taskQueue.addTask(
_device.deviceId,
'factory_reset',
{},
{} as Record<string, never>,
async (result: TaskResult) => {
return await callback(result.status === ResultStatus.Success)
}
@@ -377,9 +388,8 @@ export class DeviceActionExecutor {
const allParams = device.getParameters()
const allProps = device.getProperties()
const knownPC = [...allParams, ...allProps].find((p: any) => /(^|\.)ProvisioningCode$/.test(p?.name))
const knownPC = [...allParams, ...allProps].find((p) => /(^|\.)ProvisioningCode$/.test(p?.name))
// Use the detected data model stored on the device; fall back to scanning known params
let provisioningPath: string
if (knownPC) {
provisioningPath = knownPC.name
@@ -388,13 +398,16 @@ export class DeviceActionExecutor {
} else if (device.dataModel === 'tr-098') {
provisioningPath = 'InternetGatewayDevice.DeviceInfo.ProvisioningCode'
} else {
// Legacy fallback: scan stored params
const hasDeviceTree = [...allParams, ...allProps].some((p: any) => typeof p?.name === 'string' && p.name.startsWith('Device.'))
provisioningPath = hasDeviceTree ? 'Device.DeviceInfo.ProvisioningCode' : 'InternetGatewayDevice.DeviceInfo.ProvisioningCode'
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, $: { Type: 'xsd:string' } } }]
const pvStructs = [{ Name: provisioningPath, Value: { _: device.tmpPC as string, $: { Type: 'xsd:string' } } }]
return this.taskQueue.addTask(
device.deviceId,
@@ -402,8 +415,6 @@ export class DeviceActionExecutor {
{ parameterValueList: pvStructs, parameterKey },
async (result: TaskResult) => {
if (result.status === ResultStatus.Success) {
// ProvisioningCode must always be stored as a writable parameter.
// If it was previously classified as a read-only property (wrong), remove it first.
device.removeProperty(provisioningPath)
device.setParameter(new DeviceParameter(provisioningPath, device.tmpPC, ParameterType.STRING))
await this.persistDevice(device)
+62 -47
View File
@@ -9,13 +9,15 @@ import { ModeType } from '../../domain/shared/mode.js'
import logger from '@adonisjs/core/services/logger'
import { ConfigurationEvaluationState } from './configuration_evaluation_state.js'
import { reclassifyDeviceFromCatalog } from '../parameter_writability.js'
import type { CWMPTaskResultData } from '../../protocol/tr069_types.js'
import { ObjectId } from 'mongodb'
// ─── Interfaces ────────────────────────────────────────────────────────────────
export interface DeviceContext {
device: Device
session: string
message?: { success: boolean; data: any }
message?: { success: boolean; data: CWMPTaskResultData }
taskQueue: CWMPTaskQueue
events?: string[]
services: {
@@ -54,13 +56,13 @@ class IdleState extends DeviceState {
if (device.gpnStatus === 'unknown') {
// Never classified → all tmpParams are unclassified
for (const pv of tmpParams) {
const name = typeof pv?.Name === 'string' ? pv.Name : pv?.Name?._ || null
const name = typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || null
if (name) unclassified.push(name)
}
} else {
// Already classified before → only params NOT in _parameters AND NOT in _properties are new
for (const pv of tmpParams) {
const name = typeof pv?.Name === 'string' ? pv.Name : pv?.Name?._ || null
const name = typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || null
if (!name) continue
if (!device.getParameter(name) && !device.getProperty(name)) {
unclassified.push(name)
@@ -179,14 +181,16 @@ class DiscoveringParametersState extends DeviceState {
// GPN success — process the response
if (context.message?.success && context.message.data) {
try {
context.device.organizeParameters(context.message.data, new Date())
const gpnResp = context.message.data as import('../../protocol/tr069_types.js').GetParameterNamesResponse
const infoList = gpnResp?.ParameterList?.ParameterInfoStruct ?? []
context.device.organizeParameters(infoList, new Date())
context.device.setGpnStatus('supported')
logger.info(
{ deviceId: context.device.identifier, paramCount: context.device.getParameters().length },
'[DiscoveringParameters] GPN response processed'
)
} catch (e: any) {
logger.error({ error: e?.message }, '[DiscoveringParameters] Error processing GPN response')
} catch (e: unknown) {
logger.error({ error: (e as Error)?.message }, '[DiscoveringParameters] Error processing GPN response')
}
}
@@ -241,9 +245,9 @@ class CriticalReadsState extends DeviceState {
criticalPaths,
async (result) => {
if (result.status === 'success') {
const resp: any = result.data
const list = resp?.ParameterList?.ParameterValueStruct
if (Array.isArray(list) && list.length > 0) {
const resp = result.data as import('../../protocol/tr069_types.js').GetParameterValuesResponse
const list = resp?.ParameterList?.ParameterValueStruct ?? []
if (list.length > 0) {
context.device.setTmpParamValues(list)
}
}
@@ -256,20 +260,20 @@ class CriticalReadsState extends DeviceState {
// After GPV response: process values from the response, then reclassify
if (context.message?.success && context.message.data) {
try {
const resp = context.message.data
const list = resp?.ParameterList?.ParameterValueStruct
if (Array.isArray(list) && list.length > 0) {
const resp = context.message.data as import('../../protocol/tr069_types.js').GetParameterValuesResponse
const list = resp?.ParameterList?.ParameterValueStruct ?? []
if (list.length > 0) {
// Normalize Name fields: XMLParser may return {_: "path", "$xsi:type": "..."} when Name has attributes
const normalized = list.map((pv: any) => ({
const normalized = list.map((pv) => ({
...pv,
Name: typeof pv?.Name === 'string' ? pv.Name : pv?.Name?._ || pv?.Name,
Name: typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || pv?.Name,
}))
context.device.setTmpParamValues(normalized)
context.device.updateFromValues(normalized)
logger.info({ count: normalized.length, names: normalized.map((p: any) => p.Name) }, '[CriticalReads] GPV response values processed')
logger.info({ count: normalized.length, names: normalized.map((p) => p.Name) }, '[CriticalReads] GPV response values processed')
}
} catch (e: any) {
logger.error({ error: e?.message }, '[CriticalReads] Error processing GPV response values')
} catch (e: unknown) {
logger.error({ error: (e as Error)?.message }, '[CriticalReads] Error processing GPV response values')
}
}
@@ -281,9 +285,9 @@ class CriticalReadsState extends DeviceState {
export class ExecutingCommandsState extends DeviceState {
name = 'executing-commands'
private commandsQueue: any[] = []
private commandsQueue: Record<string, unknown>[] = []
private started = false
private currentCommandRecord: any | null = null
private currentCommandRecord: Record<string, unknown> | null = null
private includeNI: boolean
constructor(includeNI: boolean = true) {
@@ -332,10 +336,11 @@ export class ExecutingCommandsState extends DeviceState {
// Optimistic update for SPV
if (this.currentCommandRecord.action === CommandType.SET_PARAMETER_VALUES) {
const params = Array.isArray(this.currentCommandRecord.parameters) ? this.currentCommandRecord.parameters : []
const pvStructs = params.map((p: any) => ({
Name: p?.name,
Value: { _: p?.value, $: { Type: 'xsd:string' } },
const rawList = Array.isArray(this.currentCommandRecord.parameters) ? this.currentCommandRecord.parameters as Array<{ name?: string; value?: unknown }> : []
const pvStructs: import('../../protocol/tr069_types.js').ParameterValueStruct[] = rawList.map((p) => ({
'@class': '.ParameterValueStruct',
Name: p?.name ?? '',
Value: { '@class': '.ValueObject', _: String(p?.value ?? ''), $: { '@class': '.ValueObjectAttribute', Type: 'xsd:string' } },
}))
if (pvStructs.length > 0) {
context.device.updateFromValues(pvStructs)
@@ -347,10 +352,10 @@ export class ExecutingCommandsState extends DeviceState {
}
try {
await CommandModel.updateCommand(this.currentCommandRecord._id, {
status: this.currentCommandRecord.status,
executedAt: now,
completedAt: this.currentCommandRecord.completedAt,
await CommandModel.updateCommand(this.currentCommandRecord._id as ObjectId, {
status: this.currentCommandRecord.status as CommandStatus | undefined,
executedAt: this.currentCommandRecord.executedAt as Date | undefined,
completedAt: this.currentCommandRecord.completedAt as Date | undefined,
updatedAt: now,
})
} catch (err) {
@@ -382,7 +387,7 @@ export class ExecutingCommandsState extends DeviceState {
commandRecord.status = CommandStatus.SENT
commandRecord.executedAt = now
try {
await CommandModel.updateCommand(commandRecord._id, { status: CommandStatus.SENT, executedAt: now, updatedAt: now })
await CommandModel.updateCommand(commandRecord._id as ObjectId, { status: CommandStatus.SENT, executedAt: now, updatedAt: now })
} catch (err) {
logger.error(`Failed to mark command as SENT: ${err}`)
}
@@ -393,53 +398,63 @@ export class ExecutingCommandsState extends DeviceState {
} catch {}
const action = commandRecord.action as CommandType
const params = commandRecord.parameters
// params is typed as unknown from MongoDB — we cast to a flexible record for access
const params = commandRecord.parameters as Record<string, unknown> | string | unknown[] | undefined
// Map CommandType to CWMP request
switch (action) {
case CommandType.GET_PARAMETER_VALUES: {
const names: string[] = Array.isArray(params) ? params : params?.names || params?.paths || params?.parameterNames || []
const p = params as Record<string, unknown> | string[] | undefined
const names: string[] = Array.isArray(p) ? (p as string[]) : ((p as Record<string, unknown>)?.names as string[] || (p as Record<string, unknown>)?.paths as string[] || (p as Record<string, unknown>)?.parameterNames as string[] || [])
return context.services.deviceAction.getParameterValues(context.device, names, () => Promise.resolve(''))
}
case CommandType.GET_PARAMETER_NAMES: {
const name: string = typeof params === 'string' ? params : (params?.name || params?.path || params?.parameterPath || '')
const p = params as Record<string, unknown> | string | undefined
const name: string = typeof p === 'string' ? p : ((p as Record<string, unknown>)?.name as string || (p as Record<string, unknown>)?.path as string || (p as Record<string, unknown>)?.parameterPath as string || '')
return context.services.deviceAction.getParameterNames(context.device, name, () => Promise.resolve(''))
}
case CommandType.GET_PARAMETER_ATTRIBUTES: {
const names: string[] = Array.isArray(params) ? params : params?.names || params?.paths || params?.parameterNames || []
const p = params as Record<string, unknown> | string[] | undefined
const names: string[] = Array.isArray(p) ? (p as string[]) : ((p as Record<string, unknown>)?.names as string[] || (p as Record<string, unknown>)?.paths as string[] || (p as Record<string, unknown>)?.parameterNames as string[] || [])
return context.services.deviceAction.getParameterAttributes(context.device, names, () => Promise.resolve(''))
}
case CommandType.SET_PARAMETER_VALUES: {
const rawParams = Array.isArray(params)
? params
: params?.parameterValues || params?.parameters || []
const spvParams = rawParams.map((p: any) => ({
parameterName: p.parameterName ?? p.name,
value: p.value,
type: p.type,
const p = params as Record<string, unknown> | Array<{ parameterName?: string; name?: string; value: unknown; type?: string }> | undefined
const rawParams: Array<{ parameterName?: string; name?: string; value: unknown; type?: string }> = Array.isArray(p)
? p
: ((p as Record<string, unknown>)?.parameterValues as typeof rawParams || (p as Record<string, unknown>)?.parameters as typeof rawParams || [])
const spvParams = rawParams.map((pv) => ({
parameterName: pv.parameterName ?? pv.name ?? '',
value: pv.value as string | number | boolean,
type: pv.type,
}))
return context.services.deviceAction.applySetParameterValues(context.device, spvParams, () => Promise.resolve(''))
}
case CommandType.SET_PARAMETER_ATTRIBUTES: {
const spaParams = Array.isArray(params) ? params : params?.attributes || params?.parameterAttributes || []
return context.services.deviceAction.applySetParameterAttributes(context.device, spaParams, () => Promise.resolve(''))
const p = params as Record<string, unknown> | unknown[] | undefined
const spaParams = Array.isArray(p) ? p : ((p as Record<string, unknown>)?.attributes as unknown[] || (p as Record<string, unknown>)?.parameterAttributes as unknown[] || [])
return context.services.deviceAction.applySetParameterAttributes(context.device, spaParams as import('../../services/cwmp/device_action_executor.js').SetParameterAttribute[], () => Promise.resolve(''))
}
case CommandType.DOWNLOAD: {
return context.services.deviceAction.applyDownload(context.device, params || {}, () => Promise.resolve(''))
const p = (params as Record<string, unknown>) || {}
return context.services.deviceAction.applyDownload(context.device, p as unknown as import('../../services/cwmp/device_action_executor.js').DownloadParameter, () => Promise.resolve(''))
}
case CommandType.UPLOAD: {
return context.services.deviceAction.applyUpload(context.device, params || {}, () => Promise.resolve(''))
const p = (params as Record<string, unknown>) || {}
return context.services.deviceAction.applyUpload(context.device, p as unknown as import('../../services/cwmp/device_action_executor.js').UploadParameter, () => Promise.resolve(''))
}
case CommandType.REBOOT: {
return context.services.deviceAction.applyReboot(context.device, params || {}, () => Promise.resolve(''))
const p = (params as Record<string, unknown>) || {}
return context.services.deviceAction.applyReboot(context.device, p as import('../../services/cwmp/device_action_executor.js').RebootParameter, () => Promise.resolve(''))
}
case CommandType.FACTORY_RESET: {
return context.services.deviceAction.applyFactoryReset(context.device, {}, () => Promise.resolve(''))
}
case CommandType.SCHEDULE_INFORM: {
const p = (params as Record<string, unknown>) || {}
return context.services.deviceAction.applyScheduleInform(
context.device,
{ delayInSeconds: params?.delayInSeconds || 300, commandKey: params?.commandKey },
{ delayInSeconds: (p?.delayInSeconds as number) || 300, commandKey: p?.commandKey as string | undefined },
() => Promise.resolve('')
)
}
@@ -458,7 +473,7 @@ export class ExecutingCommandsState extends DeviceState {
logger.error(`Unknown command type: ${action}`)
// Mark as failed
commandRecord.status = CommandStatus.FAILED
await CommandModel.updateCommand(commandRecord._id, { status: CommandStatus.FAILED, updatedAt: new Date() })
await CommandModel.updateCommand(commandRecord._id as ObjectId, { status: CommandStatus.FAILED, updatedAt: new Date() })
this.currentCommandRecord = null
if (this.commandsQueue.length > 0) {
return this.processNextCommand(context)
@@ -578,7 +593,7 @@ export class MainStateMachine {
return this.executeCurrentState()
}
async handleResponse(success: boolean, message: any): Promise<string> {
async handleResponse(success: boolean, message: CWMPTaskResultData): Promise<string> {
this.context.message = { success, data: message }
return this.executeCurrentState()
}
+2 -2
View File
@@ -485,7 +485,7 @@ export class StateMachineManager extends EventEmitter {
/**
* Handle a CWMP response message
*/
async handleCWMPResponse(session: string, success: boolean, message: any): Promise<string> {
async handleCWMPResponse(session: string, success: boolean, message: import('./../../protocol/tr069_types.js').CWMPTaskResultData): Promise<string> {
const machineId = this.sessionMachines.get(session)
if (!machineId) return ''
@@ -545,7 +545,7 @@ export class StateMachineManager extends EventEmitter {
/**
* Enqueue an immediate task for a device (e.g., after Inform)
*/
async enqueueImmediate(device: Device, type: string, payload: any): Promise<void> {
async enqueueImmediate(device: Device, type: import('./../../protocol/tr069_types.js').CWMPTaskType, payload: import('./../../protocol/tr069_types.js').CWMPTaskPayload): Promise<void> {
this.taskQueue.addCWMPTask(device.deviceId, type, payload)
}
+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)