Aller au contenu

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 :

  1. 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.
  2. Entrez un nom de serveur (par exemple, "Prod-East", "Staging-Lab", "Client-Acme")
  3. Saisissez l'URL du serveur (par exemple, https://stentor-prod.example.com/api)
  4. L'interface utilisateur valide l'URL en sondant GET /v1/health avec un délai d'attente de 10 secondes.
  5. Entrez votre e-mail et votre mot de passe pour ce serveur
  6. 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 :

  1. Les profils de serveur sont réhydratés à partir de localStorage (clé : stentor-connections)
  2. Seuls les champs sérialisables persistent : id, name, url, accessToken, color
  3. Champs d'exécution réinitialisés aux valeurs par défaut : status='disconnected', wsConnected=false, user=null
  4. Si le accessToken stocké 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 serverId est 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 :

  1. Vérifie un cache client (un ServerApiClient par ID de serveur)
  2. S'il n'est pas mis en cache, appelle useConnectionStore.getState().connections[serverId] pour rechercher l'URL et le jeton du serveur
  3. Crée un ServerApiClient configuré avec l'URL de base correcte
  4. 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-ticket avant 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 serverId avant 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 :

  1. Cliquez avec le bouton droit sur le serveur dans la barre de commutation
  2. Sélectionnez Reconnecter
  3. 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é.