2026-10-08 09:35:16 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"log"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Un Hub représente une seule connexion amont vers nova-serialproxy, pour une
|
|
|
|
|
// seule VM (identifiée par son instance_id). La console série n'accepte
|
|
|
|
|
// qu'une connexion à la fois : le Hub est le seul à parler à nova-serialproxy,
|
|
|
|
|
// et duplique ce qu'il reçoit vers tous les navigateurs abonnés (l'élève qui
|
|
|
|
|
// écrit, le prof qui regarde en lecture seule).
|
|
|
|
|
type Hub struct {
|
|
|
|
|
instanceID string
|
|
|
|
|
upstream *websocket.Conn
|
|
|
|
|
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
subscribers map[*Client]bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Client struct {
|
|
|
|
|
conn *websocket.Conn
|
|
|
|
|
role string // "ecriture" ou "lecture"
|
|
|
|
|
hub *Hub
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
hubsMu sync.Mutex
|
|
|
|
|
hubs = map[string]*Hub{}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var upstreamDialer = websocket.Dialer{
|
|
|
|
|
HandshakeTimeout: 10 * time.Second,
|
|
|
|
|
Subprotocols: []string{"binary"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// obtenirHub renvoie le Hub existant pour cette instance, ou en ouvre un
|
|
|
|
|
// nouveau (une seule connexion amont) si c'est le premier abonné.
|
|
|
|
|
func obtenirHub(instanceID, urlAmont string) (*Hub, error) {
|
|
|
|
|
hubsMu.Lock()
|
|
|
|
|
defer hubsMu.Unlock()
|
|
|
|
|
|
|
|
|
|
if hub, ok := hubs[instanceID]; ok {
|
|
|
|
|
return hub, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
upstream, _, err := upstreamDialer.Dial(urlAmont, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hub := &Hub{
|
|
|
|
|
instanceID: instanceID,
|
|
|
|
|
upstream: upstream,
|
|
|
|
|
subscribers: map[*Client]bool{},
|
|
|
|
|
}
|
|
|
|
|
hubs[instanceID] = hub
|
|
|
|
|
go hub.pompeAmont()
|
|
|
|
|
|
|
|
|
|
log.Printf("console %s : connexion amont ouverte", instanceID)
|
|
|
|
|
|
|
|
|
|
return hub, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// pompeAmont lit en continu ce que renvoie nova-serialproxy et le recopie
|
|
|
|
|
// vers tous les navigateurs abonnés à cette VM.
|
|
|
|
|
func (h *Hub) pompeAmont() {
|
|
|
|
|
defer h.fermer()
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
messageType, donnees, err := h.upstream.ReadMessage()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h.mu.Lock()
|
|
|
|
|
for client := range h.subscribers {
|
|
|
|
|
_ = client.conn.WriteMessage(messageType, donnees)
|
|
|
|
|
}
|
|
|
|
|
h.mu.Unlock()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *Hub) ajouter(c *Client) {
|
|
|
|
|
h.mu.Lock()
|
|
|
|
|
h.subscribers[c] = true
|
|
|
|
|
h.mu.Unlock()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// retirer enlève l'abonné ; si c'était le dernier, la connexion amont est
|
|
|
|
|
// fermée (plus personne ne regarde cette VM, pas de raison de la garder).
|
|
|
|
|
func (h *Hub) retirer(c *Client) {
|
|
|
|
|
h.mu.Lock()
|
|
|
|
|
delete(h.subscribers, c)
|
|
|
|
|
vide := len(h.subscribers) == 0
|
|
|
|
|
h.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
if vide {
|
|
|
|
|
h.fermer()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *Hub) fermer() {
|
|
|
|
|
hubsMu.Lock()
|
|
|
|
|
if hubs[h.instanceID] == h {
|
|
|
|
|
delete(hubs, h.instanceID)
|
|
|
|
|
}
|
|
|
|
|
hubsMu.Unlock()
|
|
|
|
|
|
|
|
|
|
_ = h.upstream.Close()
|
|
|
|
|
log.Printf("console %s : connexion amont fermée", h.instanceID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// pompeClient lit ce que tape le navigateur et le relaie vers nova-serialproxy
|
|
|
|
|
// — uniquement si ce client a le rôle "ecriture". Un abonné en lecture seule
|
|
|
|
|
// ne peut rien envoyer : ses frappes sont simplement ignorées.
|
|
|
|
|
func (c *Client) pompeClient() {
|
|
|
|
|
defer c.hub.retirer(c)
|
|
|
|
|
defer c.conn.Close()
|
|
|
|
|
|
|
|
|
|
for {
|
ajoute le stock chaud, la correction automatique et les nouveaux essais de création de stack
Stock de stacks prêtes par lab dans Redis (prise atomique en Lua, ménage
par comparaison avec Heat, disjoncteur), files gérées par Horizon, séances
planifiables. Correcteur Go sur l'hyperviseur : socket Unix, commandes lues
dans le lab.yaml uniquement, exécution par l'agent invité QEMU. Une création
de stack ratée est supprimée et réessayée deux fois, puis la session passe
en échec, visible du prof.
Corrige aussi trois défauts des étapes précédentes : la console se coupait
à la première frappe d'un navigateur (trames texte), l'élève ne pouvait pas
rejoindre une séance, la déconnexion n'avait pas de jeton CSRF.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:52:16 +02:00
|
|
|
_, donnees, err := c.conn.ReadMessage()
|
2026-10-08 09:35:16 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if c.role != "ecriture" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
ajoute le stock chaud, la correction automatique et les nouveaux essais de création de stack
Stock de stacks prêtes par lab dans Redis (prise atomique en Lua, ménage
par comparaison avec Heat, disjoncteur), files gérées par Horizon, séances
planifiables. Correcteur Go sur l'hyperviseur : socket Unix, commandes lues
dans le lab.yaml uniquement, exécution par l'agent invité QEMU. Une création
de stack ratée est supprimée et réessayée deux fois, puis la session passe
en échec, visible du prof.
Corrige aussi trois défauts des étapes précédentes : la console se coupait
à la première frappe d'un navigateur (trames texte), l'élève ne pouvait pas
rejoindre une séance, la déconnexion n'avait pas de jeton CSRF.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:52:16 +02:00
|
|
|
// Toujours en binaire vers l'amont : xterm.js envoie des trames texte,
|
|
|
|
|
// et nova-serialproxy (sous-protocole « binary ») ferme la connexion
|
|
|
|
|
// à la première trame texte reçue (constaté le 2026-10-08, étape 11).
|
2026-10-08 09:35:16 +02:00
|
|
|
c.hub.mu.Lock()
|
ajoute le stock chaud, la correction automatique et les nouveaux essais de création de stack
Stock de stacks prêtes par lab dans Redis (prise atomique en Lua, ménage
par comparaison avec Heat, disjoncteur), files gérées par Horizon, séances
planifiables. Correcteur Go sur l'hyperviseur : socket Unix, commandes lues
dans le lab.yaml uniquement, exécution par l'agent invité QEMU. Une création
de stack ratée est supprimée et réessayée deux fois, puis la session passe
en échec, visible du prof.
Corrige aussi trois défauts des étapes précédentes : la console se coupait
à la première frappe d'un navigateur (trames texte), l'élève ne pouvait pas
rejoindre une séance, la déconnexion n'avait pas de jeton CSRF.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:52:16 +02:00
|
|
|
err = c.hub.upstream.WriteMessage(websocket.BinaryMessage, donnees)
|
2026-10-08 09:35:16 +02:00
|
|
|
c.hub.mu.Unlock()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// N'accepte que les connexions dont l'en-tête Origin correspond au site qui
|
|
|
|
|
// sert la page (reflété via Nginx) : empêche un autre site de faire ouvrir
|
|
|
|
|
// une console depuis le navigateur d'un élève à son insu.
|
|
|
|
|
var upgrader = websocket.Upgrader{
|
|
|
|
|
CheckOrigin: func(r *http.Request) bool {
|
|
|
|
|
origine := r.Header.Get("Origin")
|
|
|
|
|
if origine == "" {
|
|
|
|
|
return true // outils en ligne de commande, pas de navigateur
|
|
|
|
|
}
|
|
|
|
|
return strings.HasSuffix(origine, "://"+r.Host)
|
|
|
|
|
},
|
|
|
|
|
}
|