// 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 }