Files
adam-acs/docs/api/tr069-exchanges.yaml
T
Kevin L'Hereec 0c83a4ccb2 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
2026-06-03 15:52:44 +02:00

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