- 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
476 lines
17 KiB
YAML
476 lines
17 KiB
YAML
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
|