Files
adam-acs/app/middleware/cwmp_logger_middleware.ts
T
Kevin L'HereecandCopilot 847da1cdf5 feat: per-device TR-069 SOAP trace logging
- Tr069TraceService: in-memory Map O(1) lookup, Promise-queue per device
  for sequential file writes, 10MB rotation (5 files kept)
- config/tr069-trace.json: persisted list of traced devices (atomic write)
- CwmpLoggerMiddleware: buffers request+response, writes to device log file
  after next() once ctx.cwmpDeviceId is set by controller
- CwmpController: sets ctx.cwmpDeviceId on all code paths (Inform + others)
- StateMachineManager: getHashIdForRemote() for non-Inform messages
- REST API: GET/POST/DELETE /api/v1/tr069-trace
- UI: Tr069Trace Vue page with device search, add/remove tracing
- 8 new unit tests (192 total, all passing)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01 12:07:07 +02:00

83 lines
2.4 KiB
TypeScript

import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
// @ts-ignore - xml-formatter types conflict with ESM interop
import formatXml from 'xml-formatter'
import { Tr069TraceService } from '#services/tr069_trace_service'
/**
* CWMP Logger Middleware
*
* - Logs all SOAP exchanges to stdout for debugging.
* - For traced devices, writes full SOAP payloads to logs/tr069/{deviceId}.log.
*/
export default class CwmpLoggerMiddleware {
private prettyXml(xml: string): string {
try {
return (formatXml as any)(xml, {
indentation: ' ',
collapseContent: true,
lineSeparator: '\n',
})
} catch {
return xml
}
}
private logToStdout(direction: string, sessionId: string, xml: string) {
const timestamp = new Date().toISOString()
const separator = '─'.repeat(80)
process.stdout.write(
`\n${separator}\n` +
`[TR-069] ${direction} | ${timestamp} | ${sessionId}\n` +
`${separator}\n` +
`${xml}\n` +
`${separator}\n\n`
)
}
async handle(ctx: HttpContext, next: NextFn) {
const { request, response } = ctx
const rawBody = request.raw() || ''
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}`
if (rawBody && rawBody.trim() !== '') {
this.logToStdout('CPE→ACS', sessionId, this.prettyXml(rawBody))
}
let capturedResponseXml: string | null = null
const originalSend = response.send.bind(response)
response.send = (body: any) => {
if (typeof body === 'string' && body.includes('soap-env:Envelope')) {
this.logToStdout('ACS→CPE', sessionId, this.prettyXml(body))
capturedResponseXml = body
}
return originalSend(body)
}
await next()
const deviceId = ctx.cwmpDeviceId
if (!deviceId) {
return
}
const traceService = Tr069TraceService.getInstance()
if (!traceService.isTraced(deviceId)) {
return
}
if (rawBody && rawBody.trim() !== '') {
traceService.writeExchange(deviceId, 'CPE→ACS', this.prettyXml(rawBody))
}
if (capturedResponseXml) {
traceService.writeExchange(deviceId, 'ACS→CPE', this.prettyXml(capturedResponseXml))
}
}
}