- 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>
83 lines
2.4 KiB
TypeScript
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))
|
|
}
|
|
}
|
|
}
|