import type { HttpContext } from '@adonisjs/core/http' import type { NextFn } from '@adonisjs/core/types/http' import { parseSoapXml, extractCwmpMessage } from '../protocol/soap.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 ? Object.keys(message.data) : [], }, 'CWMP XML parsed' ) } catch (error: any) { logger.error({ sessionId, error: error.message }, '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: any | null message: { type: string; data: any } | null isEmpty: boolean } } }