157 lines
3.5 KiB
Go
157 lines
3.5 KiB
Go
|
|
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 {
|
||
|
|
messageType, donnees, err := c.conn.ReadMessage()
|
||
|
|
if err != nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if c.role != "ecriture" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
c.hub.mu.Lock()
|
||
|
|
err = c.hub.upstream.WriteMessage(messageType, donnees)
|
||
|
|
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)
|
||
|
|
},
|
||
|
|
}
|