Files
adam-acs/app/protocol/soap.ts
T
Kevin L'Hereec f3eb8e1650 feat: restore strong typing on TR-069/CWMP protocol layer
- Copy cwmp-1-0..4.ts source classes from cwmp-connector (papaye) into
  app/protocol/ as local files (ts-nocheck, external dependency)
- Add soap_envelope.ts: SoapEnvelope, SoapEnvelopeHeader, CwmpID, NAMESPACES
- Add tr069_types.ts: re-exports all CWMP classes + CwmpMessage discriminated
  union + typed task payload interfaces (CWMPTaskType, CWMPTaskPayload, etc.)
- Rewrite cwmp_message_builder.ts to use class-based builders instead of raw
  XML template strings; add transferCompleteResponse() and soapFault()
- soap.ts: extractCwmpMessage returns CwmpMessage union, adds runtime allowlist
  guard against unknown CWMP method names from CPE
- cwmp_middleware.ts: envelope typed SoapEnvelope|null, message CwmpMessage|null
- cwmp_controller.ts: handleInform(informData: Inform), remove any, migrate
  TransferComplete and fault to new builder path, drop buildSoapEnvelope import
- task_queue.ts: CWMPTask.type → CWMPTaskType literal union, payload →
  CWMPTaskPayload, result → CWMPTaskResultData
- device_action_executor.ts: GPV/GPN/GPA responses typed, no more any
- main_state_machine.ts: DeviceContext.message.data → CWMPTaskResultData,
  organizeParameters receives ParameterInfoStruct[] directly
- device.ts: tmpParamValues typed ParameterValueStruct[], organizeParameters
  signature updated, updateFromValues typed
- device_id.ts: fromInform typed with DeviceIdStruct
- soap_envelope.ts: remove unconditional <cwmp:noMoreRequests> header
  (CPE→ACS only per TR-069 §3.7.1.4)
2026-07-20 15:25:26 +02:00

119 lines
3.8 KiB
TypeScript

import { XMLParser } from 'fast-xml-parser'
import type { CwmpMessage, SoapEnvelope, SoapHeader } from './tr069_types.js'
export { type SoapEnvelope, type SoapHeader, type CwmpMessage } from './tr069_types.js'
export const CWMP_VERSIONS: Record<string, string> = {
'urn:dslforum-org:cwmp-1-0': '1.0',
'urn:dslforum-org:cwmp-1-1': '1.1',
'urn:dslforum-org:cwmp-1-2': '1.2',
'urn:dslforum-org:cwmp-1-3': '1.3',
'urn:dslforum-org:cwmp-1-4': '1.4',
}
const xmlParser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '$',
textNodeName: '_',
numberParseOptions: { leadingZeros: false, hex: false },
isArray: (name) => {
const arrayTags = ['ParameterValueStruct', 'ParameterInfoStruct', 'EventStruct', 'string']
return arrayTags.includes(name)
},
})
export function parseSoapXml(xml: string): SoapEnvelope {
const parsed = xmlParser.parse(xml) as Record<string, unknown>
const envelope =
(parsed['soap-env:Envelope'] as Record<string, unknown>) ||
(parsed['SOAP-ENV:Envelope'] as Record<string, unknown>) ||
(parsed['soapenv:Envelope'] as Record<string, unknown>) ||
{}
const header =
(envelope['soap-env:Header'] as Record<string, unknown>) ||
(envelope['SOAP-ENV:Header'] as Record<string, unknown>) ||
(envelope['soapenv:Header'] as Record<string, unknown>) ||
{}
const body =
(envelope['soap-env:Body'] as Record<string, unknown>) ||
(envelope['SOAP-ENV:Body'] as Record<string, unknown>) ||
(envelope['soapenv:Body'] as Record<string, unknown>) ||
{}
// Detect CWMP version from namespace attributes
let cwmpVersion = '1.0'
for (const [, value] of Object.entries(envelope)) {
if (typeof value === 'string' && value.startsWith('urn:dslforum-org:cwmp-')) {
cwmpVersion = CWMP_VERSIONS[value] || '1.0'
break
}
}
const soapHeader: SoapHeader = {}
const idNode = header['cwmp:ID'] || header['cwmp:Id']
if (idNode) {
soapHeader.id =
typeof idNode === 'string'
? idNode
: (idNode as { _?: string })?._ || String(idNode)
}
return { header: soapHeader, body, cwmpVersion }
}
/** Types de méthodes CWMP acceptés en entrée (CPE→ACS) */
const ALLOWED_CWMP_TYPES = new Set([
'Inform',
'TransferComplete',
'GetParameterNamesResponse',
'GetParameterValuesResponse',
'GetParameterAttributesResponse',
'SetParameterValuesResponse',
'SetParameterAttributesResponse',
'RebootResponse',
'FactoryResetResponse',
'DownloadResponse',
'UploadResponse',
'ScheduleInformResponse',
'Fault',
])
export function extractCwmpMessage(body: Record<string, unknown>): CwmpMessage | null {
for (const key of Object.keys(body)) {
if (key.startsWith('cwmp:')) {
const type = key.replace('cwmp:', '')
if (!ALLOWED_CWMP_TYPES.has(type)) return null
const data = body[key]
return { type, data } as CwmpMessage
}
}
// 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] } as CwmpMessage
}
return null
}
export function buildSoapEnvelope(cwmpId: string | undefined, bodyContent: string): string {
const id = cwmpId || Math.random().toString(36).substring(2, 10)
return `<?xml version="1.0" encoding="UTF-8"?>
<soap-env:Envelope
xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:soap-enc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cwmp="urn:dslforum-org:cwmp-1-0">
<soap-env:Header>
<cwmp:ID soap-env:mustUnderstand="1">${id}</cwmp:ID>
</soap-env:Header>
<soap-env:Body>
${bodyContent}
</soap-env:Body>
</soap-env:Envelope>`
}