Gestion multi-serveur¶
L'interface utilisateur de l'opérateur de Stentor prend en charge la connexion simultanée à plusieurs instances de serveur à partir d'une seule session de navigateur. Ceci est essentiel pour les opérateurs qui gèrent des missions distinctes, maintiennent des environnements de préparation et de production ou assurent la coordination entre les régions géographiques.
Cette page documente l'architecture de gestion des connexions, le système d'agrégation de données et les modèles opérationnels pour les flux de travail multi-serveurs.
Présentation de l'architecture¶
Chaque connexion serveur est totalement indépendante : jetons d'authentification distincts, connexions WebSocket distinctes, magasins de données distincts. L'interface utilisateur regroupe les données de tous les serveurs connectés dans des vues unifiées tout en préservant les métadonnées d'origine du serveur.
graph TB
subgraph Browser ["Operator Browser"]
UI["Stentor UI"]
Store["Connection Store<br/><small>Zustand + localStorage</small>"]
end
subgraph ServerA ["Server A (Production)"]
API_A["REST API"]
WS_A["CockpitHub WS"]
end
subgraph ServerB ["Server B (Staging)"]
API_B["REST API"]
WS_B["CockpitHub WS"]
end
subgraph ServerC ["Server C (Region 2)"]
API_C["REST API"]
WS_C["CockpitHub WS"]
end
UI --> Store
Store -.->|"Token A"| API_A
Store -.->|"Token B"| API_B
Store -.->|"Token C"| API_C
UI <-->|"Events"| WS_A
UI <-->|"Events"| WS_B
UI <-->|"Events"| WS_C Gestion des connexions¶
Ajout d'un serveur¶
Utilisez la boîte de dialogue de connexion au serveur pour ajouter un nouveau serveur. Fournissez un nom descriptif et l'URL de l'API du serveur, puis authentifiez-vous avec vos informations d'identification pour ce serveur.
Étapes :
- Cliquez sur le bouton ++ dans la barre de changement de serveur (visible lorsqu'il existe plus de 2 serveurs) ou ouvrez la boîte de dialogue de connexion à partir du menu des paramètres.
- Entrez un nom de serveur (par exemple, "Prod-East", "Staging-Lab", "Client-Acme")
- Saisissez l'URL du serveur (par exemple,
https://stentor-prod.example.com/api) - L'interface utilisateur valide l'URL en sondant
GET /v1/healthavec un délai d'attente de 10 secondes. - Entrez votre e-mail et votre mot de passe pour ce serveur
- En cas de connexion réussie, la connexion est stockée et une connexion WebSocket est établie
Chaque serveur se voit automatiquement attribuer un badge de couleur parmi une palette de 8 couleurs (bleu, rouge, vert, ambre, violet, rose, cyan, orange). Les couleurs parcourent la palette en fonction du nombre de connexions.
Profils de serveur¶
Chaque connexion au serveur suit l'état suivant :
interface ServerConnection {
id: string // UUID (auto-generated)
name: string // User-assigned label
url: string // Base API URL
status: ServerStatus // 'connected' | 'disconnected' | 'connecting' | 'error'
error?: string // Error message when status is 'error'
accessToken: string | null // JWT access token for this server
user: ServerUser | null // Authenticated user { id, email }
wsConnected: boolean // Whether the CockpitHub WebSocket is active
lastSeen: number // Timestamp of last successful interaction
color: string // UI badge color (hex, auto-assigned)
}
Cycle de vie du statut :
| Statut | Signification |
|---|---|
disconnected | Non connecté (état initial ou après déconnexion manuelle) |
connecting | Authentification en cours |
connected | Authentifié avec un jeton valide et un WebSocket actif |
error | Échec de la connexion (erreur réseau, informations d'identification non valides, jeton expiré) |
Cycle de vie de la connexion¶
Le flux de connexion complet, depuis l'ajout d'un serveur jusqu'à la réception d'événements en temps réel :
sequenceDiagram
participant Op as Operator
participant UI as Stentor UI
participant Store as Connection Store
participant Server as Target Server
Op->>UI: Add server (name, URL, credentials)
UI->>Store: addServer(name, url) -> id
Store->>Store: Assign color from palette
UI->>Store: setServerStatus(id, 'connecting')
UI->>Server: GET /v1/health
Server-->>UI: { status: 'ok', server: 'stentor' }
UI->>Server: POST /v1/auth/login
Server-->>UI: { access_token, user }
UI->>Store: setServerAuth(id, tokens, user)
Store->>Store: status = 'connected'
UI->>Server: POST /v1/auth/ws-ticket
Server-->>UI: { ticket: '...' }
UI->>Server: WS /v1/cockpit/ws?ticket=...
Server-->>UI: 101 Switching Protocols
UI->>Store: setWsStatus(id, true)
loop Real-time Events
Server->>UI: SequencedEvent (tagged with serverId)
end Persistance : Le magasin de connexions utilise Zustand avec la persistance localStorage. Lors du rechargement de la page :
- Les profils de serveur sont réhydratés à partir de
localStorage(clé :stentor-connections) - Seuls les champs sérialisables persistent :
id,name,url,accessToken,color - Champs d'exécution réinitialisés aux valeurs par défaut :
status='disconnected',wsConnected=false,user=null - Si le
accessTokenstocké est toujours valide, l'interface utilisateur se reconnecte automatiquement
Changer de serveur actif¶
Le serveur actif détermine quelles données du serveur sont affichées dans les vues d'un seul serveur (par exemple, le shell du cockpit cible les beacons d'un serveur spécifique).
- La Barre de commutation de serveur apparaît sous la forme d'une barre inférieure fixe lorsqu'il existe plus de deux serveurs.
- Cliquez sur un bouton de serveur pour le définir comme actif (cliquez à nouveau pour désélectionner)
- Chaque bouton affiche :
- Un point de couleur correspondant à la couleur attribuée au serveur
- Un indicateur d'état WebSocket (vert = connecté)
- Le nom du serveur (double-cliquez pour renommer en ligne)
- Une bordure d'état (vert = connecté, orange = connexion, rouge = erreur)
- Cliquez avec le bouton droit sur un serveur pour accéder aux options du menu contextuel : Renommer, Déconnecter, Supprimer, Reconnecter
Agrégation de données¶
Comment ça marche¶
Le hook useMultiServerQuery est le mécanisme d’agrégation principal. Il diffuse la même requête en parallèle sur tous les serveurs connectés, puis fusionne les résultats dans une liste unifiée avec les métadonnées d'origine du serveur.
Sous le capot, il utilise useQueries de TanStack React Query pour créer une requête par serveur connecté. Chaque requête obtient une clé préfixée par le serveur (par exemple, [serverId, 'beacons']) pour une mise en cache et une invalidation indépendantes.
// Example: Fetch beacons from all connected servers
const { data, isLoading, errors } = useMultiServerQuery(
['beacons'],
(apiClient) => apiClient.get('/v1/c2/beacons'),
{ refetchInterval: 5000 }
);
// data is AggregatedItem<Beacon>[]
// Each item carries server origin metadata
data.forEach(item => {
console.log(`[${item.serverName}] ${item.data.hostname} (${item.data.status})`);
});
Wrapper AggregatedItem¶
Chaque élément renvoyé par une requête multi-serveur est enveloppé dans une enveloppe AggregatedItem qui identifie de quel serveur il provient :
interface AggregatedItem<T> {
serverId: string // Server connection UUID
serverName: string // User-assigned label (e.g., "Prod-East")
serverColor: string // Badge color (hex, e.g., "#3b82f6")
data: T // The actual data item from this server
}
Ce wrapper permet à l'interface utilisateur de :
- Afficher des badges de couleur à côté des éléments indiquant leur origine du serveur
- Filtrer les vues sur les données d'un seul serveur
- Trier par serveur dans des tableaux agrégés
- Acheminer les commandes vers le bon serveur lorsque vous agissez sur un élément
Quelles vues regroupent¶
Toutes les pages n'affichent pas de données multi-serveurs. Certaines vues sont intrinsèquement spécifiques au serveur (par exemple, une session shell interactive cible un beacon sur un serveur).
| Pages | Mode | Description |
|---|---|---|
| Liste des beacons | Multi-serveur | Affiche les beacons de tous les serveurs connectés avec des badges de couleur |
| Liste des listeners | Multi-serveur | Affiche les listeners de tous les serveurs |
| Liste des informations d'identification | Multi-serveur | Identifiants récoltés agrégés |
| Liste cible | Multi-serveur | Tous les hôtes découverts au cours des engagements |
| Télécharger la liste | Multi-serveur | Fichiers téléchargés depuis tous les serveurs |
| Shell de cockpit | Serveur unique | Le shell interactif cible les beacons du serveur actif |
| Console de scripts | Serveur unique | Gestion des scripts CNA sur serveur actif |
| Générateur de payload | Serveur unique | Génère des payloads via le relais du serveur actif |
Gestion des échecs partiels¶
Si un serveur est inaccessible ou renvoie une erreur, les données des autres serveurs s'affichent toujours normalement. Le résultat de la requête multi-serveur fournit un rapport d'erreurs granulaire :
interface MultiServerQueryResult<T> {
data: AggregatedItem<T>[] // Items from ALL responding servers
isLoading: boolean // True if ANY server query is loading
isError: boolean // True if ANY server query has errored
errors: ServerError[] // Per-server error details
serverResults: Map<string, ServerResult<T>> // Per-server breakdown
}
interface ServerError {
serverId: string
serverName: string
error: Error
}
La nouvelle tentative automatique est gérée par le mécanisme de nouvelle tentative intégré de React Query. Les requêtes serveur ayant échoué sont réessayées indépendamment sans affecter les requêtes réussies.
Opérations spécifiques au serveur¶
useServerQuery Hook¶
Pour les vues qui ciblent un seul serveur, le hook useServerQuery fournit des requêtes à l'échelle du serveur :
// Fetch data from a specific server only
const { data, isLoading } = useServerQuery(
activeServerId,
['beacons', beaconId, 'tasks'],
(apiClient) => apiClient.get(`/v1/c2/beacons/${beaconId}/tasks`)
);
Le crochet :
- Ajoute
serverIdà la clé de requête pour l'isolation du cache - Désactive la requête si
serverIdest vide/null - Renvoie le résultat standard de la requête React (non agrégé)
Routage des clients API¶
Chaque connexion au serveur dispose d'un client API dédié qui gère l'authentification et le routage des URL. Consultez la Référence de l'API REST pour le catalogue complet des points de terminaison :
// Get a cached API client for a specific server
import { getApiClientForServer } from '@/lib/api-client';
const client = getApiClientForServer(serverId);
const beacons = await client.get('/v1/c2/beacons');
La fonction getApiClientForServer :
- Vérifie un cache client (un
ServerApiClientpar ID de serveur) - S'il n'est pas mis en cache, appelle
useConnectionStore.getState().connections[serverId]pour rechercher l'URL et le jeton du serveur - Crée un
ServerApiClientconfiguré avec l'URL de base correcte - Le client attache automatiquement l'en-tête
Authorization: Bearer <token>du magasin de connexions
WebSocket par serveur¶
Chaque serveur connecté obtient sa propre connexion WebSocket indépendante via le hook useServerWebSocket :
- Dérivation de l'URL : L'URL WebSocket est créée à partir de l'URL de l'API de base configurée du serveur (et non de
window.location) - Authentification basée sur les tickets : Récupère un nouveau ticket auprès de
POST /v1/auth/ws-ticketavant chaque connexion - Interruption exponentielle : La reconnexion utilise une interruption avec instabilité (1 s initiales, 30 s maximum, instabilité aléatoire jusqu'à 500 ms)
- Cycle de vie indépendant : La déconnexion du WebSocket d'un serveur n'affecte pas les autres
- Marquage d'événements : Tous les événements entrants sont marqués avec
serverIdavant d'être envoyés aux rappels.
interface CockpitCallbacks {
onBeaconUpdate?: (update: BeaconUpdate & { serverId: string }) => void
onConsoleLog?: (entry: ConsoleLogEntry & { serverId: string }) => void
onTaskStarted?: (event: TaskEvent & { serverId: string }) => void
onTaskCompleted?: (event: TaskEvent & { serverId: string }) => void
onShellOutput?: (output: ShellOutputEvent & { serverId: string }) => void
onFileTransferProgress?: (progress: FileTransferProgress & { serverId: string }) => void
// ... additional event callbacks
}
Chaque rappel reçoit le payload de l'événement augmentée d'un champ serverId, permettant aux gestionnaires d'acheminer les événements vers le contexte de serveur correct.
Conseils opérationnels¶
Conventions de dénomination
Nommer les serveurs de manière descriptive pour les distinguer d'un seul coup d'œil dans des vues agrégées :
- Par engagement : "Client-Acme", "Client-Widget", "Internal-Lab"
- Par environnement : "Prod-East", "Staging", "Dev-Local"
- Par fonction : "Accès initial", "Persistance", "Exfil"
Les noms de serveurs peuvent être modifiés à tout moment en double-cliquant sur le nom dans la barre de changement de serveur.
Insignes de couleur
Chaque serveur se voit automatiquement attribuer une couleur parmi une palette de 8 couleurs. Dans les vues agrégées (liste de beacons, liste d'identifiants), les éléments affichent un point de couleur indiquant leur serveur d'origine. Cela fournit un contexte visuel instantané lors de l’analyse de grandes listes.
La palette de couleurs tourne : blue, red, green, amber, violet, pink, cyan, orange.
Données du navigateur
L'état de connexion (profils de serveur et jetons) persiste dans localStorage sous la clé stentor-connections. Pour réinitialiser toutes les connexions :
- Effacer le stockage local du site dans les DevTools du navigateur
- Ou utilisez une fenêtre privée/incognito pour une nouvelle session
Pour les opérations multi-engagement véritablement isolées (par exemple, différents clients qui ne doivent jamais partager l'état du navigateur), utilisez des profils de navigateur distincts.
Expiration du jeton
Si le jeton d'accès d'un serveur expire pendant que vous êtes connecté, l'état du serveur passe à "error" et les appels d'API vers ce serveur échoueront. L'opérateur doit se réauthentifier :
- Cliquez avec le bouton droit sur le serveur dans la barre de commutation
- Sélectionnez Reconnecter
- Entrez les informations d'identification dans la boîte de dialogue de connexion
Les autres connexions au serveur ne sont pas affectées : une défaillance partielle est isolée par serveur.
Mode serveur unique
Si un seul serveur est configuré, la barre de commutation de serveur est masquée et l'interface utilisateur se comporte exactement comme une application traditionnelle à serveur unique. Les fonctionnalités multi-serveurs s'activent automatiquement lorsqu'un deuxième serveur est ajouté.