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:
1 parent
8969355688
commit
f3eb8e1650
19 files changed
+2745
-384
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
@@ -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 ?? ''),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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/>'
|
||||
}
|
||||
}
|
||||
@@ -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>`
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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>`
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user