From f3eb8e165060767f47f7ba1371d67323486d4872 Mon Sep 17 00:00:00 2001 From: Kevin L'Hereec Date: Mon, 20 Jul 2026 15:25:26 +0200 Subject: [PATCH] feat: restore strong typing on TR-069/CWMP protocol layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 header (CPE→ACS only per TR-069 §3.7.1.4) --- app/controllers/cwmp_controller.ts | 129 +- app/domain/device/device.ts | 38 +- app/domain/device/device_id.ts | 2 +- app/domain/device/device_record.ts | 8 +- app/middleware/cwmp_middleware.ts | 12 +- app/protocol/common/cwmp_message_builder.ts | 269 +++-- app/protocol/cwmp-1-0.ts | 1055 +++++++++++++++++ app/protocol/cwmp-1-1.ts | 221 ++++ app/protocol/cwmp-1-2.ts | 489 ++++++++ app/protocol/cwmp-1-3.ts | 30 + app/protocol/cwmp-1-4.ts | 69 ++ app/protocol/soap.ts | 74 +- app/protocol/soap_envelope.ts | 199 ++++ app/protocol/tr069_types.ts | 205 ++++ .../cwmp/configuration_evaluation_state.ts | 18 +- app/services/cwmp/device_action_executor.ts | 91 +- app/services/cwmp/main_state_machine.ts | 109 +- app/services/cwmp/state_machine_manager.ts | 4 +- app/services/cwmp/task_queue.ts | 107 +- 19 files changed, 2745 insertions(+), 384 deletions(-) create mode 100644 app/protocol/cwmp-1-0.ts create mode 100644 app/protocol/cwmp-1-1.ts create mode 100644 app/protocol/cwmp-1-2.ts create mode 100644 app/protocol/cwmp-1-3.ts create mode 100644 app/protocol/cwmp-1-4.ts create mode 100644 app/protocol/soap_envelope.ts create mode 100644 app/protocol/tr069_types.ts diff --git a/app/controllers/cwmp_controller.ts b/app/controllers/cwmp_controller.ts index 1bd268c..11371e2 100644 --- a/app/controllers/cwmp_controller.ts +++ b/app/controllers/cwmp_controller.ts @@ -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 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, - '' - ) - 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 = { lastRemoteIp: cpeIpForStorage } + const dbUpdate: Record = { 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 = ` - Server - ${reason} - - - 8002 - ${reason} - - - ` - return buildSoapEnvelope(undefined, body) + return CWMPMessageBuilder.soapFault(FaultCodeType.ACSInternalError, reason) } private async loadDevice(deviceId: DeviceId): Promise { @@ -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 } } diff --git a/app/domain/device/device.ts b/app/domain/device/device.ts index 3768830..94530f8 100644 --- a/app/domain/device/device.ts +++ b/app/domain/device/device.ts @@ -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 = new Map() private _properties: Map = 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 | 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 diff --git a/app/domain/device/device_id.ts b/app/domain/device/device_id.ts index 45c8e8a..15192d0 100644 --- a/app/domain/device/device_id.ts +++ b/app/domain/device/device_id.ts @@ -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 ?? ''), diff --git a/app/domain/device/device_record.ts b/app/domain/device/device_record.ts index 2d2b51c..d225ca9 100644 --- a/app/domain/device/device_record.ts +++ b/app/domain/device/device_record.ts @@ -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 { diff --git a/app/middleware/cwmp_middleware.ts b/app/middleware/cwmp_middleware.ts index 6a95136..3b3943e 100644 --- a/app/middleware/cwmp_middleware.ts +++ b/app/middleware/cwmp_middleware.ts @@ -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 } } diff --git a/app/protocol/common/cwmp_message_builder.ts b/app/protocol/common/cwmp_message_builder.ts index 277c2a1..67b996c 100644 --- a/app/protocol/common/cwmp_message_builder.ts +++ b/app/protocol/common/cwmp_message_builder.ts @@ -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 = ` - 1 - ` - 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 = ` - ${parameterPath} - ${nextLevel ? '1' : '0'} - ` - 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) => `${n}`).join('\n ') - const body = ` - - ${names} - - ` - 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) => ` - ${p.name} - ${p.value} - ` - ) - .join('\n ') - const body = ` - - ${params} - - papaye-v2 - ` - 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 = ` - ${params.commandKey || ''} - ${params.fileType || '1 Firmware Upgrade Image'} - ${params.url} - ${params.username || ''} - ${params.password || ''} - ${params.fileSize || 0} - ${params.targetFileName || ''} - ${params.delaySeconds || 0} - ${params.successUrl || ''} - ${params.failureUrl || ''} - ` - 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 = ` - ${params.commandKey || ''} - ${params.fileType || '3 Vendor Configuration File'} - ${params.url} - ${params.username || ''} - ${params.password || ''} - ${params.delaySeconds || 0} - ` - 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 = ` - ${commandKey || ''} - ` - 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 = `` - 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 = ` - ${delaySeconds} - ${commandKey || ''} - ` - 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) => `${n}`).join('\n ') - const body = ` - - ${names} - - ` - 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) => ` - ${a.name} - ${a.notificationChange ? '1' : '0'} - ${a.notification} - ${a.accessListChange ? '1' : '0'} - - ${a.accessList.map((s) => `${s}`).join('')} - - ` - ) - .join('') - const body = ` - - ${attrs} - - ` - 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 { diff --git a/app/protocol/cwmp-1-0.ts b/app/protocol/cwmp-1-0.ts new file mode 100644 index 0000000..5e10b0e --- /dev/null +++ b/app/protocol/cwmp-1-0.ts @@ -0,0 +1,1055 @@ +// @ts-nocheck — fichier copié de cwmp-connector (papaye) +export class Cwmp_1_0 { + [key: string]: any + + public '@class' = '.Cwmp_1_0' + public ID?: ID + public HoldRequests?: HoldRequests + public Fault?: Fault + public GetRPCMethods?: GetRPCMethods + public GetRPCMethodsResponse?: GetRPCMethodsResponse + public SetParameterValues?: SetParameterValues + public SetParameterValuesResponse?: SetParameterValuesResponse + public GetParameterValues?: GetParameterValues + public GetParameterValuesResponse?: GetParameterValuesResponse + public GetParameterNames?: GetParameterNames + public GetParameterNamesResponse?: GetParameterNamesResponse + public SetParameterAttributes?: SetParameterAttributes + public SetParameterAttributesResponse?: SetParameterAttributesResponse + public GetParameterAttributes?: GetParameterAttributes + public GetParameterAttributesResponse?: GetParameterAttributesResponse + public AddObject?: AddObject + public AddObjectResponse?: AddObjectResponse + public DeleteObject?: DeleteObject + public DeleteObjectResponse?: DeleteObjectResponse + public Download?: Download + public DownloadResponse?: DownloadResponse + public Reboot?: Reboot + public RebootResponse?: RebootResponse + public GetQueuedTransfers?: GetQueuedTransfers + public GetQueuedTransfersResponse?: GetQueuedTransfersResponse + public ScheduleInform?: ScheduleInform + public ScheduleInformResponse?: ScheduleInformResponse + public SetVouchers?: SetVouchers + public SetVouchersResponse?: SetVouchersResponse + public GetOptions?: GetOptions + public GetOptionsResponse?: GetOptionsResponse + public Upload?: Upload + public UploadResponse?: UploadResponse + public FactoryReset?: FactoryReset + public FactoryResetResponse?: FactoryResetResponse + public Inform?: Inform + public InformResponse?: InformResponse + public TransferComplete?: TransferComplete + public TransferCompleteResponse?: TransferCompleteResponse + public Kicked?: Kicked + public KickedResponse?: KickedResponse + public RequestDownload?: RequestDownload + public RequestDownloadResponse?: RequestDownloadResponse + + constructor(props: any = {}) { + this.ID = props.ID ? new ID(props.ID) : undefined + this.HoldRequests = props.HoldRequests ? new HoldRequests(props.HoldRequests) : undefined + this.Fault = props.Fault ? new Fault(props.Fault) : undefined + this.GetRPCMethods = props.GetRPCMethods ? new GetRPCMethods(props.GetRPCMethods) : undefined + this.GetRPCMethodsResponse = props.GetRPCMethodsResponse ? new GetRPCMethodsResponse(props.GetRPCMethodsResponse) : undefined + this.SetParameterValues = props.SetParameterValues ? new SetParameterValues(props.SetParameterValues) : undefined + this.SetParameterValuesResponse = props.SetParameterValuesResponse ? new SetParameterValuesResponse(props.SetParameterValuesResponse) : undefined + this.GetParameterValues = props.GetParameterValues ? new GetParameterValues(props.GetParameterValues) : undefined + this.GetParameterValuesResponse = props.GetParameterValuesResponse ? new GetParameterValuesResponse(props.GetParameterValuesResponse) : undefined + this.GetParameterNames = props.GetParameterNames ? new GetParameterNames(props.GetParameterNames) : undefined + this.GetParameterNamesResponse = props.GetParameterNamesResponse ? new GetParameterNamesResponse(props.GetParameterNamesResponse) : undefined + this.SetParameterAttributes = props.SetParameterAttributes ? new SetParameterAttributes(props.SetParameterAttributes) : undefined + this.SetParameterAttributesResponse = props.SetParameterAttributesResponse ? new SetParameterAttributesResponse(props.SetParameterAttributesResponse) : undefined + this.GetParameterAttributes = props.GetParameterAttributes ? new GetParameterAttributes(props.GetParameterAttributes) : undefined + this.GetParameterAttributesResponse = props.GetParameterAttributesResponse ? new GetParameterAttributesResponse(props.GetParameterAttributesResponse) : undefined + this.AddObject = props.AddObject ? new AddObject(props.AddObject) : undefined + this.AddObjectResponse = props.AddObjectResponse ? new AddObjectResponse(props.AddObjectResponse) : undefined + this.DeleteObject = props.DeleteObject ? new DeleteObject(props.DeleteObject) : undefined + this.DeleteObjectResponse = props.DeleteObjectResponse ? new DeleteObjectResponse(props.DeleteObjectResponse) : undefined + this.Download = props.Download ? new Download(props.Download) : undefined + this.DownloadResponse = props.DownloadResponse ? new DownloadResponse(props.DownloadResponse) : undefined + this.Reboot = props.Reboot ? new Reboot(props.Reboot) : undefined + this.RebootResponse = props.RebootResponse ? new RebootResponse(props.RebootResponse) : undefined + this.GetQueuedTransfers = props.GetQueuedTransfers ? new GetQueuedTransfers(props.GetQueuedTransfers) : undefined + this.GetQueuedTransfersResponse = props.GetQueuedTransfersResponse ? new GetQueuedTransfersResponse(props.GetQueuedTransfersResponse) : undefined + this.ScheduleInform = props.ScheduleInform ? new ScheduleInform(props.ScheduleInform) : undefined + this.ScheduleInformResponse = props.ScheduleInformResponse ? new ScheduleInformResponse(props.ScheduleInformResponse) : undefined + this.SetVouchers = props.SetVouchers ? new SetVouchers(props.SetVouchers) : undefined + this.SetVouchersResponse = props.SetVouchersResponse ? new SetVouchersResponse(props.SetVouchersResponse) : undefined + this.GetOptions = props.GetOptions ? new GetOptions(props.GetOptions) : undefined + this.GetOptionsResponse = props.GetOptionsResponse ? new GetOptionsResponse(props.GetOptionsResponse) : undefined + this.Upload = props.Upload ? new Upload(props.Upload) : undefined + this.UploadResponse = props.UploadResponse ? new UploadResponse(props.UploadResponse) : undefined + this.FactoryReset = props.FactoryReset ? new FactoryReset(props.FactoryReset) : undefined + this.FactoryResetResponse = props.FactoryResetResponse ? new FactoryResetResponse(props.FactoryResetResponse) : undefined + this.Inform = props.Inform ? new Inform(props.Inform) : undefined + this.InformResponse = props.InformResponse ? new InformResponse(props.InformResponse) : undefined + this.TransferComplete = props.TransferComplete ? new TransferComplete(props.TransferComplete) : undefined + this.TransferCompleteResponse = props.TransferCompleteResponse ? new TransferCompleteResponse(props.TransferCompleteResponse) : undefined + this.Kicked = props.Kicked ? new Kicked(props.Kicked) : undefined + this.KickedResponse = props.KickedResponse ? new KickedResponse(props.KickedResponse) : undefined + this.RequestDownload = props.RequestDownload ? new RequestDownload(props.RequestDownload) : undefined + this.RequestDownloadResponse = props.RequestDownloadResponse ? new RequestDownloadResponse(props.RequestDownloadResponse) : undefined + } +} + +export class ID { + public '@class' = '.ID' + public MustUnderstand: string = '1' + public _: string // Content textuel de l'élément ID + + constructor(props: any = {}) { + this.MustUnderstand = props.MustUnderstand || '1' + this._ = props._ || props.value || '' + } + + getXml(): string { + return `${this._}` + } +} + +export class HoldRequests { + public '@class' = '.HoldRequests' + public MustUnderstand: string = '1' + public _: boolean // Contenu boolean de l'élément HoldRequests + + constructor(props: any = {}) { + this.MustUnderstand = props.MustUnderstand || '1' + this._ = props._ !== undefined ? props._ : (props.value !== undefined ? props.value : false) + } + + getXml(): string { + return `${this._}` + } +} + +export class Fault { + public '@class' = '.SoapFault' + public Faultcode: string + public Faultstring: string + public Detail?: FaultDetail + + constructor(props: any = {}) { + this.Faultcode = props.Faultcode || 'Server' + this.Faultstring = props.Faultstring || 'Internal Error' + this.Detail = props?.Detail + } + + getXml() { + return ` + ${this.Faultcode} + ${this.Faultstring} + + ${this.Detail?.getXml() || ''} + + ` + } +} + +export class FaultDetail { + public '@class' = '.FaultDetail' + + public Fault: FaultStruct + + constructor(props: any) { + this.Fault = new FaultStruct(props.Fault) + } + + getXml() { + return '' + '' + this.Fault.FaultCode + '' + '' + this.Fault.FaultString + '' + '' + } +} + +export class SetParameterValuesFault { + public '@class' = '.SetParameterValuesFault' + public ParameterName: string + public FaultCode: number | string + public FaultString: string + + constructor(props: any) { + this.ParameterName = props.ParameterName || '' + this.FaultCode = props?.FaultCode || '' + this.FaultString = props?.FaultString || '' + } +} + +export class MethodList { + public '@class' = '.MethodList' + public String?: string[] + + constructor(props: any = {}) { + this.String = props.String + } +} + +export class FaultStruct { + public '@class' = '.FaultStruct' + public FaultCode: number + public FaultString: string + + constructor(props: { FaultCode: number; FaultString: string }) { + this.FaultCode = props.FaultCode + this.FaultString = props.FaultString || '' + } +} + +export class DeviceIdStruct { + public '@class' = '.DeviceIdStruct' + public Manufacturer: string + public OUI: string + public ProductClass: string + public SerialNumber: string + + constructor(props: any) { + this.Manufacturer = props?.Manufacturer || '' + this.OUI = props?.OUI || '' + this.ProductClass = props?.ProductClass || '' + this.SerialNumber = props?.SerialNumber || '' + } +} + +export class EventStruct { + public '@class' = '.EventStruct' + public EventCode?: EventCode + public CommandKey: string + + constructor(props: any) { + const key = (Object.keys(EventCode) as Array).find((key) => EventCode[key] === props?.EventCode) + this.EventCode = key ? EventCode[key] : undefined + this.CommandKey = props?.CommandKey || '' + } +} + +export enum EventCode { + 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', + 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 Event { + public '@class' = '.EventList' + public EventStruct: EventStruct[] + + constructor(props: any = {}) { + this.EventStruct = (props.EventStruct || []).map((item: any) => new EventStruct(item)) + } +} + +export class GetRPCMethods { + public '@class' = '.GetRPCMethods' + + constructor(props: any = {}) {} +} + +export class GetRPCMethodsResponse { + public '@class' = '.GetRPCMethodsResponse' + public MethodList?: MethodList + + constructor(props: any = {}) { + this.MethodList = props.MethodList ? new MethodList(props.MethodList) : undefined + } +} + +export class SetParameterValues { + public '@class' = '.SetParameterValues' + public ParameterValueList: ParameterValueList = new ParameterValueList({}) + public ParameterKey?: string + + constructor(props: any = {}) { + this.ParameterValueList = new ParameterValueList(props.ParameterValueList || {}) + this.ParameterKey = props.ParameterKey + } + + getXml(): string { + const parameterValueStructXml = + this.ParameterValueList?.ParameterValueStruct.map((param) => { + return ` + ${param.Name} + ${param.Value?._} + ` + }).join('') || '' + + const parameterListXml = ` + + ${parameterValueStructXml} + ` + + return ` + ${parameterListXml} + ${this.ParameterKey || ''} + ` + } +} + +export class SetParameterValuesResponse { + public '@class' = '.SetParameterValuesResponse' + public Status?: number + + constructor(props: any = {}) { + this.Status = props.Status + } +} + +export class GetParameterValues { + public '@class' = '.GetParameterValues' + public ParameterNames: string[] = [] + + constructor(props: any = {}) { + this.ParameterNames = props.ParameterNames || [] + } + + getXml() { + const parameterCount = this.ParameterNames.length + const parameterStrings = this.ParameterNames.map((p) => ` ${p}`).join('\n') + + return [``, ` `, parameterStrings, ` `, ``].join('\n') + } +} + +export class GetParameterValuesResponse { + public '@class' = '.GetParameterValuesResponse' + public ParameterList?: ParameterValueList + + constructor(props: any = {}) { + this.ParameterList = props.ParameterList ? new ParameterValueList(props.ParameterList) : undefined + } +} + +export class GetParameterNames { + public '@class' = '.GetParameterNames' + public ParameterPath?: string + public NextLevel: boolean = false + + constructor(props: any = {}) { + this.ParameterPath = props.ParameterPath + this.NextLevel = props.NextLevel || false + } + + getXml() { + return ` + ${this.ParameterPath || ''} + ${this.NextLevel ? 1 : 0} + ` + } +} + +export class GetParameterNamesResponse { + public '@class' = '.GetParameterNamesResponse' + public ParameterList?: ParameterInfoList + + constructor(props: any = {}) { + this.ParameterList = props.ParameterList ? new ParameterInfoList(props.ParameterList) : undefined + } +} + +export class SetParameterAttributes { + public '@class' = '.SetParameterAttributes' + public ParameterList?: SetParameterAttributesList + + constructor(props: any = {}) { + this.ParameterList = props.ParameterList ? new SetParameterAttributesList(props.ParameterList) : undefined + } + + getXml(): string { + const parameterAttributesXml = + this.ParameterList?.SetParameterAttributesStruct?.map((attr) => { + return ` + ${attr.Name} + ${attr.NotificationChange} + ${attr.Notification} + ${attr.AccessListChange} + ${attr.AccessList ? attr.AccessList.string!.join(', ') : ''} + ` + }).join('') || '' + + return ` + + ${parameterAttributesXml} + + ` + } +} + +export class SetParameterAttributesResponse { + public '@class' = '.SetParameterAttributesResponse' + + constructor(props: any = {}) {} +} + +export class GetParameterAttributes { + public '@class' = '.GetParameterAttributes' + public ParameterNames: string[] = [] + + constructor(props: any = {}) { + this.ParameterNames = props.ParameterNames || [] + } + + getXml(): string { + const parameterNamesXml = this.ParameterNames.map((name) => `${name}`).join('') + return ` + + ${parameterNamesXml} + + ` + } +} + +export class GetParameterAttributesResponse { + public '@class' = '.GetParameterAttributesResponse' + public ParameterList?: ParameterAttributeList + + constructor(props: any = {}) { + this.ParameterList = props.ParameterList ? new ParameterAttributeList(props.ParameterList) : undefined + } +} + +export class AddObject { + public '@class' = '.AddObject' + public ObjectName?: string + public ParameterKey?: string + + constructor(props: any = {}) { + this.ObjectName = props.ObjectName + this.ParameterKey = props.ParameterKey + } +} + +export class AddObjectResponse { + public '@class' = '.AddObjectResponse' + public InstanceNumber?: number + public Status?: number + + constructor(props: any = {}) { + this.InstanceNumber = props.InstanceNumber + this.Status = props.Status + } +} + +export class DeleteObject { + public '@class' = '.DeleteObject' + public ObjectName?: string + public ParameterKey?: string + + constructor(props: any = {}) { + this.ObjectName = props.ObjectName + this.ParameterKey = props.ParameterKey + } +} + +export class DeleteObjectResponse { + public '@class' = '.DeleteObjectResponse' + public Status?: number + + constructor(props: any = {}) { + this.Status = props.Status + } +} + +export class Download { + public '@class' = '.Download' + public CommandKey?: string + public FileType?: string + public URL?: string + public Username?: string + public Password?: string + public FileSize?: number + public TargetFileName?: string + public DelaySeconds?: number + public SuccessURL?: string + public FailureURL?: string + + 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.DelaySeconds = props.DelaySeconds + this.SuccessURL = props.SuccessURL + this.FailureURL = props.FailureURL + } + + getXml() { + const parts = [ + ``, + this.CommandKey ? `${this.CommandKey}` : '', + this.FileType ? `${this.FileType}` : '', + this.URL ? `${this.URL}` : '', + this.Username ? `${this.Username}` : '', + this.Password ? `${this.Password}` : '', + this.FileSize !== undefined ? `${this.FileSize}` : '', + this.TargetFileName ? `${this.TargetFileName}` : '', + this.DelaySeconds !== undefined ? `${this.DelaySeconds}` : '', + this.SuccessURL ? `${this.SuccessURL}` : '', + this.FailureURL ? `${this.FailureURL}` : '', + ``, + ] + return parts.filter(Boolean).join('\n') + } +} + +export class DownloadResponse { + public '@class' = '.DownloadResponse' + public Status?: number + public StartTime?: Date + public CompleteTime?: Date + + constructor(props: any = {}) { + this.Status = props.Status + this.StartTime = props.StartTime ? new Date(props.StartTime) : undefined + this.CompleteTime = props.CompleteTime ? new Date(props.CompleteTime) : undefined + } +} + +export class Reboot { + public '@class' = '.Reboot' + public CommandKey?: string + + constructor(props: any = {}) { + this.CommandKey = props.CommandKey + } + + getXml() { + return ` + ${this.CommandKey || 'RebootCommandKey'} + ` + } +} + +export class RebootResponse { + public '@class' = '.RebootResponse' + + constructor(props: any = {}) {} +} + +export class GetQueuedTransfers { + public '@class' = '.GetQueuedTransfers' + + constructor(props: any = {}) {} +} + +export class GetQueuedTransfersResponse { + public '@class' = '.GetQueuedTransfersResponse' + public TransferList?: TransferList + + constructor(props: any = {}) { + this.TransferList = props.TransferList ? new TransferList(props.TransferList) : undefined + } +} + +export class ScheduleInform { + public '@class' = '.ScheduleInform' + public DelaySeconds?: number + public CommandKey?: string + + constructor(props: any = {}) { + this.DelaySeconds = props.DelaySeconds + this.CommandKey = props.CommandKey + } + + getXml() { + return ` + ${this.DelaySeconds || 0} + ${this.CommandKey || 'ScheduleInformCommandKey'} + ` + } +} + +export class ScheduleInformResponse { + public '@class' = '.ScheduleInformResponse' + + constructor(props: any = {}) {} +} + +export class SetVouchers { + public '@class' = '.SetVouchers' + public VoucherList?: VoucherList + + constructor(props: any = {}) { + this.VoucherList = props.VoucherList ? new VoucherList(props.VoucherList) : undefined + } +} + +export class SetVouchersResponse { + public '@class' = '.SetVouchersResponse' + + constructor(props: any = {}) {} +} + +export class GetOptions { + public '@class' = '.GetOptions' + public OptionName?: string + + constructor(props: any = {}) { + this.OptionName = props.OptionName + } +} + +export class GetOptionsResponse { + public '@class' = '.GetOptionsResponse' + public OptionList?: OptionList + + constructor(props: any = {}) { + this.OptionList = props.OptionList ? new OptionList(props.OptionList) : undefined + } +} + +export class Upload { + public '@class' = '.Upload' + public CommandKey?: string + public FileType?: string + public URL?: string + public Username?: string + public Password?: string + public DelaySeconds?: number + + constructor(props: any = {}) { + this.CommandKey = props.CommandKey + this.FileType = props.FileType + this.URL = props.URL + this.Username = props.Username + this.Password = props.Password + this.DelaySeconds = props.DelaySeconds + } + + getXml() { + const parts = [ + ``, + this.CommandKey ? `${this.CommandKey}` : '', + this.FileType ? `${this.FileType}` : '', + this.URL ? `${this.URL}` : '', + this.Username ? `${this.Username}` : '', + this.Password ? `${this.Password}` : '', + this.DelaySeconds !== undefined ? `${this.DelaySeconds}` : '', + ``, + ] + return parts.filter(Boolean).join('\n') + } +} + +export class UploadResponse { + public '@class' = '.UploadResponse' + public Status?: number + public StartTime?: Date + public CompleteTime?: Date + + constructor(props: any = {}) { + this.Status = props.Status + this.StartTime = props.StartTime ? new Date(props.StartTime) : undefined + this.CompleteTime = props.CompleteTime ? new Date(props.CompleteTime) : undefined + } +} + +export class FactoryReset { + public '@class' = '.FactoryReset' + + constructor(props: any = {}) {} + + getXml() { + return `` + } +} + +export class FactoryResetResponse { + public '@class' = '.FactoryResetResponse' + + constructor(props: any) {} +} + +export class ParameterValueStruct { + public '@class' = '.ParameterValueStruct' + public Name: string + public Value?: ValueObject + + constructor(props: any) { + this.Name = props?.Name || '' + this.Value = new ValueObject(props?.Value) + } +} + +export class ValueObject { + public '@class' = '.ValueObject' + public $?: ValueObjectAttribute + public _?: any + + constructor(props: any = {}) { + this.$ = props.$ ? new ValueObjectAttribute(props.$) : undefined + this._ = props._ + } +} + +export class ValueObjectAttribute { + public '@class' = '.ValueObjectAttribute' + public Type: string + + constructor(props: any = {}) { + this.Type = props.Type + } +} + +export class ParameterValueList { + public '@class' = '.ParameterValueList' + public $: ParameterValueAttribute + public ParameterValueStruct: ParameterValueStruct[] = [] + + constructor(props: any) { + this.$ = new ParameterValueAttribute(props?.$) + this.ParameterValueStruct = (props.ParameterValueStruct || []).map((item: any) => new ParameterValueStruct(item)) + } +} + +export class ParameterValueAttribute { + public ArrayType?: string + + constructor(props: any) { + this.ArrayType = props?.ArrayType + } +} + +export class ParameterInfoStruct { + public '@class' = '.ParameterInfoStruct' + public Name: string = '' + public Writable: boolean = false + + constructor(props: any = {}) { + this.Name = props.Name || '' + const writableValue = props.Writable + if (typeof writableValue === 'boolean') { + this.Writable = writableValue + } else if (typeof writableValue === 'string') { + try { + this.Writable = JSON.parse(writableValue.toLowerCase()) + } catch { + this.Writable = false + } + } + } +} + +export class ParameterInfoList { + public '@class' = '.ParameterInfoList' + public ParameterInfoStruct: ParameterInfoStruct[] = [] + + constructor(props: any) { + this.ParameterInfoStruct = (props?.ParameterInfoStruct || []).map((item: any) => new ParameterInfoStruct(item)) + } +} + +export class ParameterNames { + public '@class' = '.ParameterNames' + public string?: string[] + + constructor(props: any = {}) { + this.string = props.string + } +} + +export class AccessList { + public '@class' = '.AccessList' + public string?: string[] + + constructor(props: any = {}) { + this.string = props.string + } +} + +export class SetParameterAttributesStruct { + public '@class' = '.SetParameterAttributesStruct' + public Name?: string + public NotificationChange?: boolean + public Notification?: number + public AccessListChange?: boolean + public AccessList?: AccessList + + constructor(props: any = {}) { + this.Name = props.Name + this.NotificationChange = props.NotificationChange || false + this.Notification = props.Notification || 1 + this.AccessListChange = props.AccessListChange || false + this.AccessList = props.AccessList ? new AccessList(props.AccessList) : undefined + } +} + +export class SetParameterAttributesList { + public '@class' = '.SetParameterAttributesList' + public SetParameterAttributesStruct?: SetParameterAttributesStruct[] + + constructor(props: any = {}) { + this.SetParameterAttributesStruct = (props.SetParameterAttributesStruct || []).map((item: any) => new SetParameterAttributesStruct(item)) + } +} + +export class ParameterAttributeStruct { + public '@class' = '.ParameterAttributeStruct' + public Name?: string + public Notification?: number + public AccessList?: AccessList + + constructor(props: any = {}) { + this.Name = props.Name + this.Notification = props.Notification + this.AccessList = props.AccessList ? new AccessList(props.AccessList) : undefined + } +} + +export class ParameterAttributeList { + public '@class' = '.ParameterAttributeList' + public ParameterAttributeStruct: ParameterAttributeStruct[] = [] + + constructor(props: any = {}) { + this.ParameterAttributeStruct = (props.ParameterAttributeStruct || []).map((item: any) => new ParameterAttributeStruct(item)) + } +} + +export class QueuedTransferStruct { + public '@class' = '.QueuedTransferStruct' + public CommandKey: string + public State: number + + constructor(props: any) { + this.CommandKey = props.CommandKey || '' + this.State = props.State || 0 + } +} + +export class TransferList { + public '@class' = '.TransferList' + public QueuedTransferStruct: QueuedTransferStruct[] = [] + + constructor(props: any) { + this.QueuedTransferStruct = (props?.QueuedTransferStruct || []).map((item: any) => new QueuedTransferStruct(item)) + } +} + +export class VoucherList { + public '@class' = '.VoucherList' + public Base64: string[] = [] + + constructor(props: any) { + this.Base64 = props?.Base64 || [] + } +} + +export class OptionStruct { + public '@class' = '.OptionStruct' + public OptionName: string + public VoucherSN: number + public State: number + public Mode: number + public StartDate: Date + public ExpirationDate: Date + public IsTransferable: number + + constructor(props: any) { + this.OptionName = props?.OptionName || '' + this.VoucherSN = props?.VoucherSN || 0 + this.State = props?.State || 0 + this.Mode = props?.Mode + this.StartDate = props?.StartDate && !isNaN(Date.parse(props?.StartDate)) ? new Date(props?.StartDate) : new Date() + this.ExpirationDate = props?.ExpirationDate && !isNaN(Date.parse(props?.ExpirationDate)) ? new Date(props?.ExpirationDate) : new Date() + this.IsTransferable = props?.IsTransferable || 1 + } +} + +export class OptionList { + public '@class' = '.OptionList' + public OptionStruct: OptionStruct[] = [] + + constructor(props: any) { + this.OptionStruct = (props?.OptionStruct || []).map((item: any) => new OptionStruct(item)) + } +} + +export class ArgStruct { + public '@class' = '.ArgStruct' + public Name: string + public Value: string + + constructor(props: any) { + this.Name = props?.Name || '' + this.Value = props?.Value || '' + } +} + +export class FileTypeArg { + public '@class' = '.FileTypeArg' + public ArgStruct: ArgStruct[] = [] + + constructor(props: any) { + this.ArgStruct = (props?.ArgStruct || []).map((item: any) => new ArgStruct(item)) + } +} + +class ParameterListAttribute { + public ArrayType: string + + constructor(props: any) { + this.ArrayType = props?.ArrayType || '' + } +} + +class ParameterList { + public '@class' = '.ParameterList' + public $?: ParameterListAttribute + public ParameterValueStruct: Array = [] + + constructor(props: any) { + this.$ = props?.$ ? new ParameterListAttribute(props?.$) : undefined + this.ParameterValueStruct = (props?.ParameterValueStruct || []).map((parameter: any) => new ParameterValueStruct(parameter)) + } +} + +export class Inform { + public '@class' = '.Inform' + public DeviceId: DeviceIdStruct + public Event: Event + public MaxEnvelopes: number + public CurrentTime: Date + public RetryCount: number + public ParameterList: ParameterList + + constructor(props: any) { + this.DeviceId = new DeviceIdStruct(props?.DeviceId) + this.Event = new Event(props?.Event) + this.MaxEnvelopes = parseInt(props?.MaxEnvelopes) || 0 + this.CurrentTime = props?.CurrentTime && !isNaN(Date.parse(props?.CurrentTime)) ? new Date(props?.CurrentTime) : new Date() + this.RetryCount = parseInt(props?.RetryCount) || 0 + this.ParameterList = new ParameterList(props?.ParameterList) + } +} + +export class InformResponse { + public '@class' = '.InformResponse' + public MaxEnvelopes: number + + constructor(props: any) { + this.MaxEnvelopes = props?.MaxEnvelopes || 0 + } + + getXml() { + return ` + + ${this.MaxEnvelopes} + + ` + } +} + +export class TransferComplete { + public '@class' = '.TransferComplete' + public CommandKey?: CommandKeyType + public FaultStruct?: FaultStruct + public StartTime?: Date + public CompleteTime?: Date + + constructor(props: any) { + this.CommandKey = props?.CommandKey + this.FaultStruct = props?.FaultStruct ? new FaultStruct(props.FaultStruct) : undefined + this.StartTime = props?.StartTime ? new Date(props.StartTime) : undefined + this.CompleteTime = props?.CompleteTime ? new Date(props.CompleteTime) : undefined + } +} + +export class TransferCompleteResponse { + public '@class' = '.TransferCompleteResponse' + + constructor(props: any) {} + + getXml() { + return '' + } +} + +export class Kicked { + public '@class' = '.Kicked' + public Command?: string + public Referer?: string + public Arg?: string + public Next?: string + + constructor(props: any) { + this.Command = props?.Command + this.Referer = props?.Referer + this.Arg = props?.Arg + this.Next = props?.Next + } +} + +export class KickedResponse { + public '@class' = '.KickedResponse' + public NextURL?: string + + constructor(props: any) { + this.NextURL = props?.NextURL + } +} + +export class RequestDownload { + public '@class' = '.RequestDownload' + public FileType?: string + public FileTypeArg?: FileTypeArg + + constructor(props: any = {}) { + this.FileType = props.FileType + this.FileTypeArg = props.FileTypeArg ? new FileTypeArg(props.FileTypeArg) : undefined + } +} + +export class RequestDownloadResponse { + public '@class' = '.RequestDownloadResponse' + + constructor(props: any = {}) {} +} + +export type CommandKeyType = string +export type ObjectNameType = string + +export enum FaultCodeType { + // CPE fault codes + 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, + + // 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 Vendor fault codes (range) + ACSVendorFaultCodeMin = 8800, + ACSVendorFaultCodeMax = 8899, +} diff --git a/app/protocol/cwmp-1-1.ts b/app/protocol/cwmp-1-1.ts new file mode 100644 index 0000000..f15bdc4 --- /dev/null +++ b/app/protocol/cwmp-1-1.ts @@ -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 `` + } +} + +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) => ` + + ${t.CommandKey} + ${t.State} + ${t.IsDownload ? 1 : 0} + ${t.FileType} + ${t.FileSize} + ${t.TargetFileName} + ` + ) + .join('') + + return ` + + ${listXml} + + ` + } +} + +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 + ? `${this.FaultStruct.FaultCode ?? 0}${this.FaultStruct.FaultString || ''}` + : '' + return ` + ${this.AnnounceURL} + ${this.TransferURL} + ${this.IsDownload ? 1 : 0} + ${this.FileType} + ${this.FileSize} + ${this.TargetFileName} + ${fault} + ${this.StartTime.toISOString()} + ${this.CompleteTime.toISOString()} + ` + } +} + +export class AutonomousTransferCompleteResponse { + public '@class' = '.AutonomousTransferCompleteResponse' + + constructor(props: any = {}) {} + + getXml() { + return '' + } +} \ No newline at end of file diff --git a/app/protocol/cwmp-1-2.ts b/app/protocol/cwmp-1-2.ts new file mode 100644 index 0000000..07ae356 --- /dev/null +++ b/app/protocol/cwmp-1-2.ts @@ -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) => ` + + ${w.WindowStart} + ${w.WindowEnd} + ${w.WindowMode} + ${w.UserMessage} + ${w.MaxRetries} + ` + ) + .join('') + + return ` + ${this.CommandKey} + ${this.FileType} + ${this.URL} + ${this.Username || ''} + ${this.Password || ''} + ${this.FileSize ?? 0} + ${this.TargetFileName || ''} + + ${twXml} + + ` + } +} + +export class ScheduleDownloadResponse { + public '@class' = '.ScheduleDownloadResponse' + constructor(props: any = {}) {} + + getXml() { + return `` + } +} + +// CancelTransfer RPC +export class CancelTransfer { + public '@class' = '.CancelTransfer' + public CommandKey: string + constructor(props: any = {}) { + this.CommandKey = props.CommandKey || '' + } + + getXml() { + return ` + ${this.CommandKey} + ` + } +} + +export class CancelTransferResponse { + public '@class' = '.CancelTransferResponse' + constructor(props: any = {}) {} + + getXml() { + return `` + } +} + +// 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 ` + ${op.URL} + ${op.UUID ? `${op.UUID}` : ''} + ${op.Username ? `${op.Username}` : ''} + ${op.Password ? `${op.Password}` : ''} + ${op.ExecutionEnvRef ? `${op.ExecutionEnvRef}` : ''} + ` + } + if (op instanceof UpdateOpStruct) { + return ` + ${op.UUID ? `${op.UUID}` : ''} + ${op.Version ? `${op.Version}` : ''} + ${op.URL ? `${op.URL}` : ''} + ${op.Username ? `${op.Username}` : ''} + ${op.Password ? `${op.Password}` : ''} + ` + } + if (op instanceof UninstallOpStruct) { + return ` + ${op.UUID} + ${op.Version ? `${op.Version}` : ''} + ${op.ExecutionEnvRef ? `${op.ExecutionEnvRef}` : ''} + ` + } + return '' + }) + .join('') + + return ` + ${opsXml} + ${this.CommandKey ? `${this.CommandKey}` : ''} + ` + } +} + +export class ChangeDUStateResponse { + public '@class' = '.ChangeDUStateResponse' + constructor(props: any = {}) {} + + getXml() { + return `` + } +} + +// 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) => ` + + ${r.UUID} + ${r.DeploymentUnitRef} + ${r.Version} + ${r.CurrentState} + ${r.Resolved ? 1 : 0} + ${r.ExecutionUnitRefList} + ${r.StartTime.toISOString()} + ${r.CompleteTime.toISOString()} + + ${r.Fault.FaultCode} + ${r.Fault.FaultString ? `${r.Fault.FaultString}` : ''} + + ` + ) + .join('') + + return ` + ${resultsXml} + ${this.CommandKey ? `${this.CommandKey}` : ''} + ` + } +} + +export class DUStateChangeCompleteResponse { + public '@class' = '.DUStateChangeCompleteResponse' + constructor(props: any = {}) {} + + getXml() { + return `` + } +} + +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) => ` + + ${r.UUID} + ${r.DeploymentUnitRef} + ${r.Version} + ${r.CurrentState} + ${r.Resolved ? 1 : 0} + ${r.ExecutionUnitRefList} + ${r.StartTime.toISOString()} + ${r.CompleteTime.toISOString()} + + ${r.Fault.FaultCode} + ${r.Fault.FaultString ? `${r.Fault.FaultString}` : ''} + + ${r.OperationPerformed} + ` + ) + .join('') + + return ` + ${resultsXml} + ` + } +} + +export class AutonomousDUStateChangeCompleteResponse { + public '@class' = '.AutonomousDUStateChangeCompleteResponse' + constructor(props: any = {}) {} + + getXml() { + return `` + } +} + +// 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 ` + ${this.AnnounceURL} + ${this.TransferURL} + ${this.IsDownload ? 1 : 0} + ${this.FileType} + ${this.FileSize} + ${this.TargetFileName} + + ${this.FaultStruct.FaultCode} + ${this.FaultStruct.FaultString} + + ${this.StartTime.toISOString()} + ${this.CompleteTime.toISOString()} + ` + } +} diff --git a/app/protocol/cwmp-1-3.ts b/app/protocol/cwmp-1-3.ts new file mode 100644 index 0000000..a8230d4 --- /dev/null +++ b/app/protocol/cwmp-1-3.ts @@ -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 + } +} diff --git a/app/protocol/cwmp-1-4.ts b/app/protocol/cwmp-1-4.ts new file mode 100644 index 0000000..c2f5f0e --- /dev/null +++ b/app/protocol/cwmp-1-4.ts @@ -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._ || '' + } +} diff --git a/app/protocol/soap.ts b/app/protocol/soap.ts index b1db3d8..9c09848 100644 --- a/app/protocol/soap.ts +++ b/app/protocol/soap.ts @@ -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 = { '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 + const envelope = + (parsed['soap-env:Envelope'] as Record) || + (parsed['SOAP-ENV:Envelope'] as Record) || + (parsed['soapenv:Envelope'] as Record) || + {} + const header = + (envelope['soap-env:Header'] as Record) || + (envelope['SOAP-ENV:Header'] as Record) || + (envelope['soapenv:Header'] as Record) || + {} + const body = + (envelope['soap-env:Body'] as Record) || + (envelope['SOAP-ENV:Body'] as Record) || + (envelope['soapenv:Body'] as Record) || + {} - // 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): 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 diff --git a/app/protocol/soap_envelope.ts b/app/protocol/soap_envelope.ts new file mode 100644 index 0000000..d474827 --- /dev/null +++ b/app/protocol/soap_envelope.ts @@ -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 = { + '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 = 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 `${this._}` + } +} + +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 = {}) { + 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 = {}) { + 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) || {}) + + const bodyProps = (props.Body as Record) || {} + 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( + `${this.Header.SessionTimeout._}` + ) + } + if (this.Header.SupportedCWMPVersions) { + headerParts.push( + `${this.Header.SupportedCWMPVersions._}` + ) + } + if (this.Header.UseCWMPVersion) { + headerParts.push( + `${this.Header.UseCWMPVersion._}` + ) + } + // noMoreRequests est un header CPE→ACS uniquement (TR-069 §3.7.1.4) — ne pas l'émettre côté ACS + + return ` + + ${headerParts.join('\n ')} + + + ${ + 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() || + '' + } + + ` + } +} diff --git a/app/protocol/tr069_types.ts b/app/protocol/tr069_types.ts new file mode 100644 index 0000000..682f64d --- /dev/null +++ b/app/protocol/tr069_types.ts @@ -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 } + | { type: 'RebootResponse'; data: Record } + | { type: 'FactoryResetResponse'; data: Record } + | { type: 'DownloadResponse'; data: Record } + | { type: 'UploadResponse'; data: Record } + | { type: 'ScheduleInformResponse'; data: Record } + | { 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 + 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 // 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 diff --git a/app/services/cwmp/configuration_evaluation_state.ts b/app/services/cwmp/configuration_evaluation_state.ts index fb2b986..728f685 100644 --- a/app/services/cwmp/configuration_evaluation_state.ts +++ b/app/services/cwmp/configuration_evaluation_state.ts @@ -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') diff --git a/app/services/cwmp/device_action_executor.ts b/app/services/cwmp/device_action_executor.ts index b8e16d2..c0713df 100644 --- a/app/services/cwmp/device_action_executor.ts +++ b/app/services/cwmp/device_action_executor.ts @@ -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 { 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, 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) diff --git a/app/services/cwmp/main_state_machine.ts b/app/services/cwmp/main_state_machine.ts index 7386853..40438b8 100644 --- a/app/services/cwmp/main_state_machine.ts +++ b/app/services/cwmp/main_state_machine.ts @@ -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[] = [] private started = false - private currentCommandRecord: any | null = null + private currentCommandRecord: Record | 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[] | 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[] | undefined + const names: string[] = Array.isArray(p) ? (p as string[]) : ((p as Record)?.names as string[] || (p as Record)?.paths as string[] || (p as Record)?.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 | undefined + const name: string = typeof p === 'string' ? p : ((p as Record)?.name as string || (p as Record)?.path as string || (p as Record)?.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[] | undefined + const names: string[] = Array.isArray(p) ? (p as string[]) : ((p as Record)?.names as string[] || (p as Record)?.paths as string[] || (p as Record)?.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 | 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)?.parameterValues as typeof rawParams || (p as Record)?.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 | unknown[] | undefined + const spaParams = Array.isArray(p) ? p : ((p as Record)?.attributes as unknown[] || (p as Record)?.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) || {} + 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) || {} + 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) || {} + 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) || {} 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 { + async handleResponse(success: boolean, message: CWMPTaskResultData): Promise { this.context.message = { success, data: message } return this.executeCurrentState() } diff --git a/app/services/cwmp/state_machine_manager.ts b/app/services/cwmp/state_machine_manager.ts index 6c21492..160615f 100644 --- a/app/services/cwmp/state_machine_manager.ts +++ b/app/services/cwmp/state_machine_manager.ts @@ -485,7 +485,7 @@ export class StateMachineManager extends EventEmitter { /** * Handle a CWMP response message */ - async handleCWMPResponse(session: string, success: boolean, message: any): Promise { + async handleCWMPResponse(session: string, success: boolean, message: import('./../../protocol/tr069_types.js').CWMPTaskResultData): Promise { 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 { + async enqueueImmediate(device: Device, type: import('./../../protocol/tr069_types.js').CWMPTaskType, payload: import('./../../protocol/tr069_types.js').CWMPTaskPayload): Promise { this.taskQueue.addCWMPTask(device.deviceId, type, payload) } diff --git a/app/services/cwmp/task_queue.ts b/app/services/cwmp/task_queue.ts index df02cff..a222c03 100644 --- a/app/services/cwmp/task_queue.ts +++ b/app/services/cwmp/task_queue.ts @@ -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> = { - 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 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 ): { 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): 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 { + async executeTaskCallback(taskId: string, status: ResultStatus, data: CWMPTaskResultData): Promise { 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)