Paillasse/correcteur/main.go

181 lines
5 KiB
Go
Raw Normal View History

// Correcteur Syllabus (étape 11) : exécute dans les VM des élèves les
// vérifications déclarées dans le lab.yaml, par l'agent invité QEMU.
//
// Il écoute sur un socket Unix accessible au seul groupe www-data (le
// portail) et n'accepte que trois identifiants : instance, lab,
// vérification. La commande exécutée est toujours lue dans le lab.yaml,
// jamais reçue du portail.
package main
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"io"
"log"
"net"
"os"
"regexp"
"strconv"
"time"
)
const (
tailleMaxRequete = 1024
delaiConnexion = 20 * time.Second
// README du pack de contenus : 10 s maximum par commande.
delaiVerification = 10 * time.Second
maxSimultanees = 8
)
var (
reInstance = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
reIdent = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,63}$`)
)
type requete struct {
Instance string `json:"instance"`
Lab string `json:"lab"`
Verification string `json:"verification"`
}
type reponse struct {
Reussi bool `json:"reussi"`
Erreur string `json:"erreur,omitempty"`
}
// Codes d'erreur renvoyés au portail (jamais la sortie de la commande).
const (
errRequete = "requete_invalide"
errLab = "lab_inconnu"
errVerification = "verification_inconnue"
errMachine = "machine_non_conforme"
errAgent = "agent_injoignable"
errDelai = "delai_depasse"
)
type config struct {
socket string
dossierLabs string
projet string
uidClient int
}
func main() {
cfg := config{
socket: envOu("CORRECTEUR_SOCKET", "/run/correcteur/correcteur.sock"),
dossierLabs: envOu("CORRECTEUR_LABS", "/opt/labs"),
projet: os.Getenv("CORRECTEUR_PROJET"),
}
uid, err := strconv.Atoi(os.Getenv("CORRECTEUR_UID_CLIENT"))
if err != nil || cfg.projet == "" {
log.Fatal("CORRECTEUR_PROJET et CORRECTEUR_UID_CLIENT sont obligatoires")
}
cfg.uidClient = uid
_ = os.Remove(cfg.socket)
ecoute, err := net.Listen("unix", cfg.socket)
if err != nil {
log.Fatalf("écoute impossible sur %s : %v", cfg.socket, err)
}
// Le groupe du processus est www-data (unité systemd) : 0660 réserve le
// socket à root et au portail.
if err := os.Chmod(cfg.socket, 0o660); err != nil {
log.Fatalf("chmod du socket : %v", err)
}
log.Printf("correcteur prêt sur %s (labs : %s)", cfg.socket, cfg.dossierLabs)
places := make(chan struct{}, maxSimultanees)
for {
conn, err := ecoute.Accept()
if err != nil {
log.Printf("accept : %v", err)
continue
}
places <- struct{}{}
go func() {
defer func() { <-places }()
traiter(cfg, conn.(*net.UnixConn))
}()
}
}
func traiter(cfg config, conn *net.UnixConn) {
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(delaiConnexion))
debut := time.Now()
// Défense en profondeur : en plus des droits du socket, on vérifie
// l'identité du processus à l'autre bout (SO_PEERCRED).
uid, err := uidPair(conn)
if err != nil || (uid != cfg.uidClient && uid != 0) {
log.Printf("connexion refusée (uid %d, %v)", uid, err)
return
}
var req requete
ligne, err := bufio.NewReader(io.LimitReader(conn, tailleMaxRequete)).ReadBytes('\n')
if err == nil {
dec := json.NewDecoder(bytes.NewReader(ligne))
dec.DisallowUnknownFields()
err = dec.Decode(&req)
}
var rep reponse
if err != nil || !reInstance.MatchString(req.Instance) || !reIdent.MatchString(req.Lab) || !reIdent.MatchString(req.Verification) {
rep = reponse{Erreur: errRequete}
} else {
rep = verifier(cfg, req)
}
sortie, _ := json.Marshal(rep)
_, _ = conn.Write(append(sortie, '\n'))
log.Printf("instance=%s lab=%s verification=%s reussi=%t erreur=%q duree=%s",
req.Instance, req.Lab, req.Verification, rep.Reussi, rep.Erreur, time.Since(debut).Round(time.Millisecond))
}
func verifier(cfg config, req requete) reponse {
lab, err := chargerLab(cfg.dossierLabs, req.Lab)
if err != nil {
return reponse{Erreur: errLab}
}
v, ok := lab.verification(req.Verification)
if !ok {
return reponse{Erreur: errVerification}
}
ctx, annuler := context.WithTimeout(context.Background(), delaiVerification+5*time.Second)
defer annuler()
dom, err := lireDomaine(ctx, req.Instance)
if err != nil {
log.Printf("dumpxml %s : %v", req.Instance, err)
return reponse{Erreur: errMachine}
}
if dom.projet() != cfg.projet || lab.machineDuNom(dom.nom()) != v.Machine {
log.Printf("instance %s refusée : projet %q, nom %q, machine attendue %q", req.Instance, dom.projet(), dom.nom(), v.Machine)
return reponse{Erreur: errMachine}
}
ctxExec, annulerExec := context.WithTimeout(ctx, delaiVerification)
defer annulerExec()
code, sortieCmd, err := executer(ctxExec, req.Instance, v.Commande)
switch {
case errors.Is(err, errDelaiDepasse):
return reponse{Erreur: errDelai}
case err != nil:
log.Printf("guest-exec %s : %v", req.Instance, err)
return reponse{Erreur: errAgent}
}
return reponse{Reussi: v.Attendu.satisfait(code, sortieCmd)}
}
func envOu(cle, defaut string) string {
if v := os.Getenv(cle); v != "" {
return v
}
return defaut
}