- 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)
115 lines
3.4 KiB
TypeScript
115 lines
3.4 KiB
TypeScript
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
|
|
*
|
|
* Handles the specifics of TR-069/CWMP requests:
|
|
* - Parses raw XML body into a SOAP envelope
|
|
* - Extracts the CWMP message from the envelope
|
|
* - Attaches parsed data to the HTTP context for the controller
|
|
* - Validates content-type (must be text/xml or empty for polling)
|
|
*/
|
|
export default class CwmpMiddleware {
|
|
async handle({ request, response, logger }: HttpContext, next: NextFn) {
|
|
const contentType = request.header('content-type') || ''
|
|
const rawBody = request.raw() || ''
|
|
|
|
// Derive session identifiers from the TCP socket
|
|
const socket = request.request?.socket
|
|
const remoteIp = request.ip()
|
|
const remotePort = socket?.remotePort || 0
|
|
const localAddress = socket?.localAddress || 'unknown'
|
|
const localPort = socket?.localPort || 0
|
|
|
|
const sessionId = `${remoteIp}:${remotePort}->${localAddress}:${localPort}`
|
|
const remoteKey = `${remoteIp}:${remotePort}`
|
|
|
|
// Attach CWMP context to the request
|
|
request.cwmp = {
|
|
sessionId,
|
|
remoteKey,
|
|
rawBody,
|
|
envelope: null,
|
|
message: null,
|
|
isEmpty: !rawBody || rawBody.trim() === '',
|
|
}
|
|
|
|
// Empty body = CPE polling for next instruction (valid in TR-069)
|
|
if (request.cwmp.isEmpty) {
|
|
logger.debug({ sessionId, remoteIp }, 'CWMP empty POST (CPE polling)')
|
|
return next()
|
|
}
|
|
|
|
logger.debug(
|
|
{
|
|
sessionId,
|
|
remoteIp,
|
|
contentType,
|
|
bodyLength: rawBody.length,
|
|
bodyPreview: rawBody.slice(0, 500),
|
|
},
|
|
'CWMP XML received'
|
|
)
|
|
|
|
// Validate content type for non-empty requests
|
|
const isXml =
|
|
contentType.includes('text/xml') ||
|
|
contentType.includes('application/xml') ||
|
|
contentType.includes('application/soap+xml') ||
|
|
contentType === '' // Some CPEs don't set content-type
|
|
|
|
if (!isXml && rawBody.trim() !== '') {
|
|
logger.warn({ sessionId, contentType }, 'CWMP invalid content-type')
|
|
response.status(415).send('Unsupported Media Type: expected text/xml')
|
|
return
|
|
}
|
|
|
|
// Parse SOAP XML
|
|
try {
|
|
const envelope = parseSoapXml(rawBody)
|
|
request.cwmp.envelope = envelope
|
|
|
|
// Extract CWMP message from envelope body
|
|
const message = extractCwmpMessage(envelope.body)
|
|
request.cwmp.message = message
|
|
|
|
logger.debug(
|
|
{
|
|
sessionId,
|
|
cwmpVersion: envelope.cwmpVersion,
|
|
cwmpId: envelope.header?.id,
|
|
messageType: message?.type ?? 'unknown',
|
|
messageKeys: message?.data && typeof message.data === 'object' ? Object.keys(message.data as object) : [],
|
|
},
|
|
'CWMP XML parsed'
|
|
)
|
|
} 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
|
|
}
|
|
|
|
return next()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extend the AdonisJS Request interface to include CWMP context.
|
|
*/
|
|
declare module '@adonisjs/core/http' {
|
|
interface HttpRequest {
|
|
cwmp: {
|
|
sessionId: string
|
|
remoteKey: string
|
|
rawBody: string
|
|
envelope: SoapEnvelope | null
|
|
message: CwmpMessage | null
|
|
isEmpty: boolean
|
|
}
|
|
}
|
|
}
|