Files
adam-acs/app/middleware/cwmp_middleware.ts
Kevin L'Hereec f9e56c78b1 refactor: migrate all internal imports to package.json subpath aliases
- Add #domain, #protocol, #utils aliases to package.json imports map
- Replace all relative imports (./, ../, ../..) in app/ and
  database/mongo_migrations/ with the corresponding #alias imports,
  eliminating relative path traversal across the codebase
- Remove dead code: ConfigurationVersionModel.findByConfigurationIdentifier
  (unused, superseded by paginateVersions) and unused exception classes
  (NotFoundException, UnprocessableEntityException, DatabaseException)
  from app/exceptions/index.ts, keeping only BadRequestException
- Fix pre-existing syntax error in configuration_version.ts (mismatched
  parenthesis) that broke tsc
- Fix eqeqeq violation in configuration_service.ts and isNaN ->
  Number.isNaN in configuration_engine.ts / cwmp-1-0.ts / exchange_controller.ts
- Fix no-shadow violation in cwmp-1-0.ts EventStruct constructor
- Prettier formatting pass on touched files

Validated: tsc --noEmit clean, eslint app/ clean, full test suite green.
2026-07-22 11:45:14 +02:00

118 lines
3.5 KiB
TypeScript

import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
import { parseSoapXml, extractCwmpMessage } from '#protocol/soap'
import type { SoapEnvelope, CwmpMessage } from '#protocol/tr069_types'
/**
* 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
}
}
}