feat: TR-069 Exchange Viewer - structured MongoDB storage with SSE streaming and sequence diagram UI
- MongoDB storage: cwmp_exchanges collection with indexes (deviceId+timestamp, deviceId+sequenceNumber unique, TTL 24h) - ExchangeStoreService: CWMP method extraction, per-device queue for ordering, retry logic, message cap (5000/device) - Exchange API: GET/DELETE/SSE stream endpoints with Last-Event-ID reconnection - CwmpLoggerMiddleware: dual-write (MongoDB + legacy files) with fire-and-forget - Frontend: SequenceViewer (CPE↔ACS lifelines, arrows, auto-scroll, virtual scroll) + InspectorPanel (formatted XML, copy, syntax highlighting) - Tr069Trace page: integrated new viewer with col-6/col-6 layout - DeviceDetail page: collapsible exchange section when device is traced - 14 property-based tests, 40+ unit tests, 5 integration tests - Health check includes index verification - Config: feature flags, SSE timeouts, retry policy
This commit is contained in:
1 parent
3fefbf6c40
commit
0c83a4ccb2
50 files changed
+15731
-43
No files matched your search
@@ -0,0 +1,475 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: TR-069 Exchange Viewer API
|
||||
description: |
|
||||
API REST pour la récupération, la suppression et le streaming en temps réel
|
||||
des échanges CWMP/TR-069 capturés lors d'une session de trace sur un device.
|
||||
|
||||
## Architecture
|
||||
|
||||
Les échanges SOAP sont capturés par le `CwmpLoggerMiddleware`, persistés dans
|
||||
MongoDB via `ExchangeStoreService`, et diffusés en temps réel via Server-Sent Events (SSE).
|
||||
|
||||
## Authentification
|
||||
|
||||
Tous les endpoints nécessitent une session authentifiée (cookie-based via AdonisJS).
|
||||
|
||||
## Feature Flags
|
||||
|
||||
| Variable d'environnement | Effet |
|
||||
|--------------------------|-------|
|
||||
| `FEATURE_EXCHANGE_PERSISTENCE=false` | Désactive l'écriture MongoDB |
|
||||
| `FEATURE_SSE_ENABLED=false` | Désactive le SSE (endpoint renvoie 503) |
|
||||
| `FEATURE_EXCHANGE_API_SOURCE=file` | Lecture depuis l'ancien système fichier |
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: adam-acs Team
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8080/api/v1
|
||||
description: Local development
|
||||
|
||||
paths:
|
||||
/tr069-trace/{deviceId}/exchanges:
|
||||
get:
|
||||
operationId: getExchanges
|
||||
summary: Récupérer les échanges CWMP d'un device
|
||||
description: |
|
||||
Retourne les échanges CWMP structurés pour un device tracé, ordonnés par
|
||||
timestamp ascending. Inclut des métadonnées sur le statut de la trace et
|
||||
la fenêtre d'enregistrement.
|
||||
|
||||
**Pagination cursor-based** : utiliser `after` pour charger les messages plus
|
||||
récents, `before` pour charger les messages plus anciens, ou `latest=true`
|
||||
pour obtenir les N messages les plus récents (chargement initial).
|
||||
tags:
|
||||
- Exchanges
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/deviceId'
|
||||
- name: limit
|
||||
in: query
|
||||
required: false
|
||||
description: |
|
||||
Nombre maximum de messages retournés.
|
||||
- Défaut : 200
|
||||
- Minimum : 1
|
||||
- Maximum : 1000
|
||||
- Retourne HTTP 400 si non-numérique ou < 1
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 1000
|
||||
default: 200
|
||||
example: 50
|
||||
- name: after
|
||||
in: query
|
||||
required: false
|
||||
description: |
|
||||
Timestamp ISO 8601 pour la pagination cursor-based (messages plus récents).
|
||||
Seuls les messages avec un timestamp strictement supérieur à cette valeur
|
||||
sont retournés. Retourne HTTP 400 si le format est invalide.
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
example: '2025-01-15T10:30:00.000Z'
|
||||
- name: before
|
||||
in: query
|
||||
required: false
|
||||
description: |
|
||||
Timestamp ISO 8601 pour la pagination inverse (messages plus anciens).
|
||||
Seuls les messages avec un timestamp strictement inférieur à cette valeur
|
||||
sont retournés. Retourne HTTP 400 si le format est invalide.
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
example: '2025-01-15T10:35:00.000Z'
|
||||
- name: latest
|
||||
in: query
|
||||
required: false
|
||||
description: |
|
||||
Si `true`, retourne les N messages les plus récents (pour le chargement
|
||||
initial). Combiné avec `limit` pour contrôler le nombre de messages.
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- 'true'
|
||||
- 'false'
|
||||
example: 'true'
|
||||
responses:
|
||||
'200':
|
||||
description: Liste des échanges avec métadonnées
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ExchangeListResponse'
|
||||
examples:
|
||||
activeTrace:
|
||||
summary: Device actuellement tracé avec des échanges
|
||||
value:
|
||||
exchanges:
|
||||
- sequenceNumber: 1
|
||||
timestamp: '2025-01-15T10:30:00.123Z'
|
||||
direction: 'CPE→ACS'
|
||||
cwmpMethod: 'Inform'
|
||||
sessionId: '10.0.0.1:54321->192.168.1.1:8080'
|
||||
rawXml: '<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:cwmp="urn:dslforum-org:cwmp-1-0"><soap-env:Body><cwmp:Inform>...</cwmp:Inform></soap-env:Body></soap-env:Envelope>'
|
||||
- sequenceNumber: 2
|
||||
timestamp: '2025-01-15T10:30:00.456Z'
|
||||
direction: 'ACS→CPE'
|
||||
cwmpMethod: 'InformResponse'
|
||||
sessionId: '10.0.0.1:54321->192.168.1.1:8080'
|
||||
rawXml: '<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:cwmp="urn:dslforum-org:cwmp-1-0"><soap-env:Body><cwmp:InformResponse><MaxEnvelopes>1</MaxEnvelopes></cwmp:InformResponse></soap-env:Body></soap-env:Envelope>'
|
||||
metadata:
|
||||
traceStatus: 'ACTIVE'
|
||||
traceStoppedAt: null
|
||||
recordedFrom: '2025-01-15T10:30:00.123Z'
|
||||
recordedUntil: '2025-01-15T10:35:42.567Z'
|
||||
total: 42
|
||||
stoppedTrace:
|
||||
summary: Trace arrêtée avec historique conservé
|
||||
value:
|
||||
exchanges:
|
||||
- sequenceNumber: 1
|
||||
timestamp: '2025-01-15T10:30:00.123Z'
|
||||
direction: 'CPE→ACS'
|
||||
cwmpMethod: 'Inform'
|
||||
sessionId: '10.0.0.1:54321->192.168.1.1:8080'
|
||||
rawXml: '<soap-env:Envelope>...</soap-env:Envelope>'
|
||||
metadata:
|
||||
traceStatus: 'STOPPED'
|
||||
traceStoppedAt: '2025-01-15T10:40:00.000Z'
|
||||
recordedFrom: '2025-01-15T10:30:00.123Z'
|
||||
recordedUntil: '2025-01-15T10:35:42.567Z'
|
||||
total: 15
|
||||
emptyTrace:
|
||||
summary: Device tracé mais aucun échange encore enregistré
|
||||
value:
|
||||
exchanges: []
|
||||
metadata:
|
||||
traceStatus: 'ACTIVE'
|
||||
traceStoppedAt: null
|
||||
recordedFrom: null
|
||||
recordedUntil: null
|
||||
total: 0
|
||||
truncatedResult:
|
||||
summary: Résultat partiel (timeout de lecture dépassé)
|
||||
value:
|
||||
exchanges: []
|
||||
metadata:
|
||||
traceStatus: 'ACTIVE'
|
||||
traceStoppedAt: null
|
||||
recordedFrom: '2025-01-15T10:30:00.123Z'
|
||||
recordedUntil: '2025-01-15T10:35:42.567Z'
|
||||
total: 5000
|
||||
isTruncated: true
|
||||
'400':
|
||||
description: Paramètre de requête invalide
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
invalidLimit:
|
||||
summary: Paramètre limit invalide
|
||||
value:
|
||||
error: 'Invalid limit parameter. Must be an integer between 1 and 1000.'
|
||||
invalidAfter:
|
||||
summary: Paramètre after invalide
|
||||
value:
|
||||
error: 'Invalid after parameter. Must be a valid ISO 8601 timestamp.'
|
||||
invalidBefore:
|
||||
summary: Paramètre before invalide
|
||||
value:
|
||||
error: 'Invalid before parameter. Must be a valid ISO 8601 timestamp.'
|
||||
'404':
|
||||
description: Le device n'a jamais été tracé (aucun échange historique)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
error: 'No trace exists for device F8B7FF-Livebox4-LB4-TEST-001'
|
||||
|
||||
delete:
|
||||
operationId: deleteExchanges
|
||||
summary: Supprimer tous les échanges d'un device
|
||||
description: |
|
||||
Supprime tous les échanges CWMP stockés pour le device spécifié.
|
||||
Retourne 204 sans corps de réponse en cas de succès.
|
||||
|
||||
Cette opération est irréversible. Si le device est actuellement tracé,
|
||||
les nouveaux échanges continueront d'être enregistrés après la suppression.
|
||||
tags:
|
||||
- Exchanges
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/deviceId'
|
||||
responses:
|
||||
'204':
|
||||
description: Échanges supprimés avec succès (pas de corps de réponse)
|
||||
|
||||
/tr069-trace/{deviceId}/exchanges/stream:
|
||||
get:
|
||||
operationId: streamExchanges
|
||||
summary: Flux SSE temps réel des échanges CWMP
|
||||
description: |
|
||||
Ouvre une connexion HTTP persistante avec `Content-Type: text/event-stream`
|
||||
qui pousse chaque nouvel échange CWMP dès qu'il est persisté.
|
||||
|
||||
## Protocole SSE
|
||||
|
||||
La connexion utilise le protocole Server-Sent Events (SSE) standard :
|
||||
|
||||
- **Content-Type** : `text/event-stream`
|
||||
- **Cache-Control** : `no-cache`
|
||||
- **Connection** : `keep-alive`
|
||||
- **X-Accel-Buffering** : `no` (pour nginx)
|
||||
|
||||
## Format des événements
|
||||
|
||||
Chaque événement SSE contient :
|
||||
```
|
||||
id: <sequenceNumber>
|
||||
data: <JSON-encoded CwmpExchange>
|
||||
|
||||
```
|
||||
|
||||
Exemple d'événement brut :
|
||||
```
|
||||
id: 42
|
||||
data: {"sequenceNumber":42,"timestamp":"2025-01-15T10:30:00.123Z","direction":"CPE→ACS","cwmpMethod":"Inform","sessionId":"10.0.0.1:54321->192.168.1.1:8080","rawXml":"<soap-env:Envelope>...</soap-env:Envelope>"}
|
||||
|
||||
```
|
||||
|
||||
## Reconnexion (Last-Event-ID)
|
||||
|
||||
Lors d'une reconnexion, le client envoie l'en-tête `Last-Event-ID` avec le
|
||||
`sequenceNumber` du dernier événement reçu. Le serveur reprend alors à partir
|
||||
du message suivant (sequenceNumber > Last-Event-ID), sans duplication.
|
||||
|
||||
Le serveur envoie un champ `retry` au début de la connexion (défaut: 3000ms)
|
||||
pour configurer l'intervalle de reconnexion automatique du client.
|
||||
|
||||
## Timeouts serveur
|
||||
|
||||
Le serveur ferme la connexion dans les cas suivants :
|
||||
- **Inactivité** : aucun nouvel échange poussé pendant 5 minutes
|
||||
- **Durée absolue** : connexion ouverte depuis plus de 2 heures
|
||||
|
||||
Dans les deux cas, le client devrait se reconnecter automatiquement
|
||||
(mécanisme natif EventSource avec Last-Event-ID).
|
||||
|
||||
## Feature Flag
|
||||
|
||||
Si `FEATURE_SSE_ENABLED=false`, cet endpoint retourne HTTP 503.
|
||||
tags:
|
||||
- Exchanges
|
||||
- SSE
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/deviceId'
|
||||
- name: Last-Event-ID
|
||||
in: header
|
||||
required: false
|
||||
description: |
|
||||
Sequence number du dernier événement reçu par le client.
|
||||
Utilisé pour reprendre le flux après une déconnexion sans duplication.
|
||||
schema:
|
||||
type: string
|
||||
example: '42'
|
||||
responses:
|
||||
'200':
|
||||
description: Flux SSE ouvert avec succès
|
||||
headers:
|
||||
Content-Type:
|
||||
schema:
|
||||
type: string
|
||||
example: text/event-stream
|
||||
Cache-Control:
|
||||
schema:
|
||||
type: string
|
||||
example: no-cache
|
||||
Connection:
|
||||
schema:
|
||||
type: string
|
||||
example: keep-alive
|
||||
X-Accel-Buffering:
|
||||
schema:
|
||||
type: string
|
||||
example: 'no'
|
||||
content:
|
||||
text/event-stream:
|
||||
schema:
|
||||
type: string
|
||||
description: Flux d'événements SSE (voir description pour le format)
|
||||
example: |
|
||||
retry: 3000
|
||||
|
||||
id: 1
|
||||
data: {"sequenceNumber":1,"timestamp":"2025-01-15T10:30:00.123Z","direction":"CPE→ACS","cwmpMethod":"Inform","sessionId":"10.0.0.1:54321->192.168.1.1:8080","rawXml":"<soap-env:Envelope>...</soap-env:Envelope>"}
|
||||
|
||||
id: 2
|
||||
data: {"sequenceNumber":2,"timestamp":"2025-01-15T10:30:00.456Z","direction":"ACS→CPE","cwmpMethod":"InformResponse","sessionId":"10.0.0.1:54321->192.168.1.1:8080","rawXml":"<soap-env:Envelope>...</soap-env:Envelope>"}
|
||||
|
||||
'404':
|
||||
description: Le device n'est pas actuellement tracé
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
error: 'Device is not being traced'
|
||||
'503':
|
||||
description: Le streaming SSE est désactivé (feature flag)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
error: 'SSE streaming is currently disabled'
|
||||
|
||||
components:
|
||||
parameters:
|
||||
deviceId:
|
||||
name: deviceId
|
||||
in: path
|
||||
required: true
|
||||
description: |
|
||||
Identifiant du device (format: `OUI-ProductClass-SerialNumber`
|
||||
ou hash MongoDB `_id`).
|
||||
schema:
|
||||
type: string
|
||||
maxLength: 128
|
||||
example: 'F8B7FF-Livebox4-LB4-TEST-001'
|
||||
|
||||
schemas:
|
||||
CwmpExchange:
|
||||
type: object
|
||||
description: Un échange SOAP CWMP individuel capturé
|
||||
required:
|
||||
- sequenceNumber
|
||||
- timestamp
|
||||
- direction
|
||||
- cwmpMethod
|
||||
- sessionId
|
||||
- rawXml
|
||||
properties:
|
||||
sequenceNumber:
|
||||
type: integer
|
||||
description: |
|
||||
Numéro de séquence monotoniquement croissant par device (commence à 1).
|
||||
Utilisé comme identifiant d'événement SSE pour la reconnexion.
|
||||
example: 42
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Horodatage UTC de la capture (ISO 8601)
|
||||
example: '2025-01-15T10:30:00.123Z'
|
||||
direction:
|
||||
type: string
|
||||
enum:
|
||||
- 'CPE→ACS'
|
||||
- 'ACS→CPE'
|
||||
description: |
|
||||
Direction du message :
|
||||
- `CPE→ACS` : requête envoyée par le CPE vers l'ACS
|
||||
- `ACS→CPE` : réponse envoyée par l'ACS vers le CPE
|
||||
example: 'CPE→ACS'
|
||||
cwmpMethod:
|
||||
type: string
|
||||
description: |
|
||||
Méthode CWMP extraite du body SOAP :
|
||||
- Nom de la méthode (ex: `Inform`, `GetParameterValues`, `SetParameterValuesResponse`)
|
||||
- `"Empty"` si le body XML est vide (empty POST du CPE)
|
||||
- `"Unknown"` si aucun élément `cwmp:` n'est trouvé dans le body
|
||||
example: 'Inform'
|
||||
sessionId:
|
||||
type: string
|
||||
description: Identifiant de session TCP (format `IP:port->IP:port`)
|
||||
example: '10.0.0.1:54321->192.168.1.1:8080'
|
||||
rawXml:
|
||||
type: string
|
||||
description: |
|
||||
Contenu XML SOAP brut du message (maximum 2 Mo).
|
||||
Peut être vide pour les messages de type "Empty".
|
||||
maxLength: 2097152
|
||||
example: '<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">...</soap-env:Envelope>'
|
||||
|
||||
ExchangeMetadata:
|
||||
type: object
|
||||
description: Métadonnées sur la session de trace du device
|
||||
required:
|
||||
- traceStatus
|
||||
- traceStoppedAt
|
||||
- recordedFrom
|
||||
- recordedUntil
|
||||
- total
|
||||
properties:
|
||||
traceStatus:
|
||||
type: string
|
||||
enum:
|
||||
- ACTIVE
|
||||
- STOPPED
|
||||
description: |
|
||||
Statut actuel de la trace :
|
||||
- `ACTIVE` : le device est en cours de trace
|
||||
- `STOPPED` : la trace a été arrêtée (historique conservé)
|
||||
example: 'ACTIVE'
|
||||
traceStoppedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
description: Timestamp d'arrêt de la trace (null si active)
|
||||
example: null
|
||||
recordedFrom:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
description: Timestamp du plus ancien échange enregistré
|
||||
example: '2025-01-15T10:30:00.123Z'
|
||||
recordedUntil:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
description: Timestamp du plus récent échange enregistré
|
||||
example: '2025-01-15T10:35:42.567Z'
|
||||
total:
|
||||
type: integer
|
||||
description: Nombre total d'échanges stockés pour ce device
|
||||
example: 42
|
||||
isTruncated:
|
||||
type: boolean
|
||||
description: |
|
||||
Présent uniquement si le résultat est partiel (timeout de lecture dépassé).
|
||||
Le client devrait réessayer avec des paramètres de pagination plus restreints.
|
||||
example: true
|
||||
|
||||
ExchangeListResponse:
|
||||
type: object
|
||||
description: Réponse de l'endpoint GET /exchanges
|
||||
required:
|
||||
- exchanges
|
||||
- metadata
|
||||
properties:
|
||||
exchanges:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/CwmpExchange'
|
||||
description: Liste des échanges ordonnés par timestamp ascending
|
||||
metadata:
|
||||
$ref: '#/components/schemas/ExchangeMetadata'
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
description: Réponse d'erreur standard
|
||||
required:
|
||||
- error
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Message d'erreur décrivant le problème
|
||||
example: 'Invalid limit parameter. Must be an integer between 1 and 1000.'
|
||||
|
||||
tags:
|
||||
- name: Exchanges
|
||||
description: Gestion des échanges CWMP capturés
|
||||
- name: SSE
|
||||
description: Streaming temps réel via Server-Sent Events
|
||||
Reference in new issue
Block a user