Split backend service into checker and generator

Both are linked through a unix socket.
This commit is contained in:
nemunaire 2023-07-10 09:17:02 +02:00
commit ed091e761c
34 changed files with 660 additions and 208 deletions

1
checker/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
checker

53
checker/choices.go Normal file
View file

@ -0,0 +1,53 @@
package main
import (
"encoding/base64"
"encoding/binary"
"encoding/json"
"io/ioutil"
"log"
"math/rand"
"os"
"srs.epita.fr/fic-server/libfic"
)
type wantChoices struct {
FlagId int `json:"id"`
}
func treatWantChoices(pathname string, team *fic.Team) {
// Generate a unique identifier to follow the request in logs
bid := make([]byte, 5)
binary.LittleEndian.PutUint32(bid, rand.Uint32())
id := "[" + base64.StdEncoding.EncodeToString(bid) + "]"
log.Println(id, "New wantChoices receive", pathname)
var ask wantChoices
if cnt_raw, err := ioutil.ReadFile(pathname); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
} else if err = json.Unmarshal(cnt_raw, &ask); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
} else if ask.FlagId == 0 {
log.Printf("%s [WRN] Invalid content in wantChoices file: %s\n", id, pathname)
os.Remove(pathname)
} else if flag, err := fic.GetFlagKey(ask.FlagId); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
} else if !team.CanSeeFlag(flag) {
log.Printf("%s [!!!] The team asks to display choices whereas it doesn't have access to the flag\n", id)
} else if exercice, err := flag.GetExercice(); err != nil {
log.Printf("%s [ERR] Unable to retrieve the flag's underlying exercice: %s\n", id, err)
} else if !team.HasAccess(exercice) {
log.Printf("%s [!!!] The team asks to display choices whereas it doesn't have access to the exercice\n", id)
} else if exercice.Disabled {
log.Println("[!!!] The team submits something for a disabled exercice")
} else if err = team.DisplayChoices(flag); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
} else {
appendGenQueue(fic.GenStruct{Id: id, Type: fic.GenTeam, TeamId: team.Id})
if err = os.Remove(pathname); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
}
}
}

52
checker/generation.go Normal file
View file

@ -0,0 +1,52 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"strings"
"time"
"srs.epita.fr/fic-server/libfic"
)
var generatorSocket string
func appendGenQueue(gs fic.GenStruct) error {
buf, err := json.Marshal(gs)
if err != nil {
return fmt.Errorf("Something is wrong with JSON encoder: %w", err)
}
sockType := "unix"
if strings.Contains(generatorSocket, ":") {
sockType = "tcp"
}
socket, err := net.Dial(sockType, generatorSocket)
if err != nil {
log.Printf("Unable to contact generator at: %s, retring in 1 second", generatorSocket)
time.Sleep(time.Second)
return appendGenQueue(gs)
}
defer socket.Close()
httpClient := &http.Client{
Transport: &http.Transport{
Dial: func(network, addr string) (net.Conn, error) {
return socket, nil
},
},
}
resp, err := httpClient.Post("http://localhost/enqueue", "application/json", bytes.NewReader(buf))
if err != nil {
return fmt.Errorf("Unable to enqueue new generation event: %w", err)
}
resp.Body.Close()
return nil
}

65
checker/hint.go Normal file
View file

@ -0,0 +1,65 @@
package main
import (
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"html"
"io/ioutil"
"log"
"math/rand"
"os"
"srs.epita.fr/fic-server/libfic"
)
type askOpenHint struct {
HintId int64 `json:"id"`
}
func treatOpeningHint(pathname string, team *fic.Team) {
// Generate a unique identifier to follow the request in logs
bid := make([]byte, 5)
binary.LittleEndian.PutUint32(bid, rand.Uint32())
id := "[" + base64.StdEncoding.EncodeToString(bid) + "]"
log.Println(id, "New openingHint receive", pathname)
var ask askOpenHint
if cnt_raw, err := ioutil.ReadFile(pathname); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
} else if err = json.Unmarshal(cnt_raw, &ask); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
} else if ask.HintId == 0 {
log.Printf("%s [WRN] Invalid content in hint file: %s\n", id, pathname)
os.Remove(pathname)
} else if hint, err := fic.GetHint(ask.HintId); err != nil {
log.Printf("%s [ERR] Unable to retrieve the given hint: %s\n", id, err)
} else if exercice, err := hint.GetExercice(); err != nil {
log.Printf("%s [ERR] Unable to retrieve the hint's underlying exercice: %s\n", id, err)
} else if exercice.Disabled {
log.Println("[!!!] The team submits something for a disabled exercice")
} else if !team.HasAccess(exercice) {
log.Printf("%s [!!!] The team asks to open an hint whereas it doesn't have access to the exercice\n", id)
} else if !team.CanSeeHint(hint) {
log.Printf("%s [!!!] The team asks to open an hint whereas it doesn't have access to it due to hint dependencies\n", id)
} else if err = team.OpenHint(hint); err != nil && !fic.DBIsDuplicateKeyError(err) { // Skip DUPLICATE KEY errors
log.Printf("%s [ERR] Unable to open hint: %s\n", id, err)
} else {
// Write event
if lvl, err := exercice.GetLevel(); err != nil {
log.Printf("%s [WRN] %s\n", id, err)
} else if theme, err := fic.GetTheme(exercice.IdTheme); err != nil {
log.Printf("%s [WRN] %s\n", id, err)
} else if _, err = fic.NewEvent(fmt.Sprintf("L'équipe %s a dévoilé un indice pour le <strong>%d<sup>e</sup></strong> défi %s&#160;!", html.EscapeString(team.Name), lvl, theme.Name), "info"); err != nil {
log.Printf("%s [WRN] Unable to create event: %s\n", id, err)
}
appendGenQueue(fic.GenStruct{Id: id, Type: fic.GenTeam, TeamId: team.Id})
appendGenQueue(fic.GenStruct{Id: id, Type: fic.GenEvents})
if err = os.Remove(pathname); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
}
}
}

90
checker/issue.go Normal file
View file

@ -0,0 +1,90 @@
package main
import (
"encoding/base64"
"encoding/binary"
"encoding/json"
"io/ioutil"
"log"
"math/rand"
"os"
"srs.epita.fr/fic-server/libfic"
)
type IssueUpload struct {
Id int64 `json:"id"`
IdExercice int64 `json:"id_exercice"`
Subject string `json:"subject"`
Description string `json:"description"`
}
func treatIssue(pathname string, team *fic.Team) {
// Generate a unique identifier to follow the request in logs
bid := make([]byte, 5)
binary.LittleEndian.PutUint32(bid, rand.Uint32())
id := "[" + base64.StdEncoding.EncodeToString(bid) + "]"
log.Println(id, "New issue receive", pathname)
var issue IssueUpload
if cnt_raw, err := ioutil.ReadFile(pathname); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
} else if err := json.Unmarshal(cnt_raw, &issue); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
} else if len(issue.Subject) == 0 && len(issue.Description) == 0 {
if err = os.Remove(pathname); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
}
log.Printf("%s Empty issue: not treated.\n", id)
} else if len(issue.Subject) == 0 {
if issue.Id <= 0 {
if err = os.Remove(pathname); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
}
log.Printf("%s Issue with no subject: not treated.\n", id)
} else if claim, err := team.GetClaim(issue.Id); err != nil {
log.Printf("%s [ERR] Team id=%d,name=%q tries to access issue id=%d, but not granted: %s.\n", id, team.Id, team.Name, issue.Id, err)
} else if len(issue.Description) == 0 {
if err = os.Remove(pathname); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
}
log.Printf("%s Empty issue: not treated.\n", id)
} else if desc, err := claim.AddDescription(issue.Description, &fic.ClaimAssignee{Id: 0}, true); err != nil {
log.Printf("%s [WRN] Unable to add description to issue: %s\n", id, err)
} else {
claim.State = "new"
claim.Update()
log.Printf("%s [OOK] New comment added to issue id=%d: id_description=%d\n", id, claim.Id, desc.Id)
if err = os.Remove(pathname); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
}
appendGenQueue(fic.GenStruct{Id: id, Type: fic.GenTeamIssues, TeamId: team.Id})
}
} else {
var exercice *fic.Exercice = nil
if e, err := fic.GetExercice(issue.IdExercice); err == nil {
exercice = e
}
if claim, err := fic.NewClaim(issue.Subject, team, exercice, nil, "medium"); err != nil {
log.Printf("%s [ERR] Unable to create new issue: %s\n", id, err)
} else if len(issue.Description) > 0 {
if _, err := claim.AddDescription(issue.Description, &fic.ClaimAssignee{Id: 0}, true); err != nil {
log.Printf("%s [WRN] Unable to add description to issue: %s\n", id, err)
} else {
log.Printf("%s [OOK] New issue created: id=%d\n", id, claim.Id)
if err = os.Remove(pathname); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
}
}
} else {
log.Printf("%s [OOK] New issue created: id=%d\n", id, claim.Id)
if err = os.Remove(pathname); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
}
}
appendGenQueue(fic.GenStruct{Id: id, Type: fic.GenTeamIssues, TeamId: team.Id})
}
}

30
checker/locked.go Normal file
View file

@ -0,0 +1,30 @@
package main
import (
"encoding/json"
"log"
"os"
"srs.epita.fr/fic-server/libfic"
)
var TeamLockedExercices = map[int64]map[string]bool{}
func treatLocked(pathname string, team *fic.Team) {
fd, err := os.Open(pathname)
if err != nil {
log.Printf("[ERR] Unable to open %q: %s", pathname, err)
return
}
var locked map[string]bool
jdec := json.NewDecoder(fd)
if err := jdec.Decode(&locked); err != nil {
log.Printf("[ERR] Unable to parse JSON %q: %s", pathname, err)
return
}
TeamLockedExercices[team.Id] = locked
log.Printf("Team %q (tid=%d) has locked %d exercices", team.Name, team.Id, len(locked))
}

221
checker/main.go Normal file
View file

@ -0,0 +1,221 @@
package main
import (
"flag"
"io/ioutil"
"log"
"math/rand"
"os"
"os/signal"
"path"
"strconv"
"strings"
"syscall"
"time"
"srs.epita.fr/fic-server/libfic"
"srs.epita.fr/fic-server/settings"
"gopkg.in/fsnotify.v1"
)
var TeamsDir string
var SubmissionDir string
func watchsubdir(watcher *fsnotify.Watcher, pathname string) error {
log.Println("Watch new directory:", pathname)
if err := watcher.Add(pathname); err != nil {
return err
}
if ds, err := ioutil.ReadDir(pathname); err != nil {
return err
} else {
for _, d := range ds {
p := path.Join(pathname, d.Name())
if d.IsDir() && d.Name() != ".tmp" && d.Mode()&os.ModeSymlink == 0 {
if err := watchsubdir(watcher, p); err != nil {
return err
}
} else if d.Mode().IsRegular() {
go treat(p)
}
}
return nil
}
}
func walkAndTreat(pathname string) error {
if ds, err := ioutil.ReadDir(pathname); err != nil {
return err
} else {
for _, d := range ds {
p := path.Join(pathname, d.Name())
if d.IsDir() && d.Name() != ".tmp" && d.Mode()&os.ModeSymlink == 0 {
if err := walkAndTreat(p); err != nil {
return err
}
} else if d.Mode().IsRegular() {
treat(p)
}
}
return nil
}
}
var ChStarted = false
var lastRegeneration time.Time
var skipInitialGeneration = false
func reloadSettings(config *settings.Settings) {
allowRegistration = config.AllowRegistration
canJoinTeam = config.CanJoinTeam
denyTeamCreation = config.DenyTeamCreation
fic.HintCoefficient = config.HintCurCoefficient
fic.WChoiceCoefficient = config.WChoiceCurCoefficient
fic.ExerciceCurrentCoefficient = config.ExerciceCurCoefficient
ChStarted = config.Start.Unix() > 0 && time.Since(config.Start) >= 0
fic.PartialValidation = config.PartialValidation
fic.UnlockedChallengeDepth = config.UnlockedChallengeDepth
fic.UnlockedChallengeUpTo = config.UnlockedChallengeUpTo
fic.DisplayAllFlags = config.DisplayAllFlags
fic.FirstBlood = config.FirstBlood
fic.SubmissionCostBase = config.SubmissionCostBase
fic.SubmissionUniqueness = config.SubmissionUniqueness
fic.GlobalScoreCoefficient = config.GlobalScoreCoefficient
fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries
fic.DiscountedFactor = config.DiscountedFactor
}
func main() {
var dsn = flag.String("dsn", fic.DSNGenerator(), "DSN to connect to the MySQL server")
flag.StringVar(&generatorSocket, "generator", "./GENERATOR/generator.socket", "Path to the generator socket")
flag.StringVar(&settings.SettingsDir, "settings", "./SETTINGSDIST", "Base directory where load and save settings")
flag.StringVar(&SubmissionDir, "submission", "./submissions", "Base directory where save submissions")
flag.StringVar(&TeamsDir, "teams", "./TEAMS", "Base directory where save teams JSON files")
var debugINotify = flag.Bool("debuginotify", false, "Show skipped inotofy events")
flag.Parse()
log.SetPrefix("[checker] ")
settings.SettingsDir = path.Clean(settings.SettingsDir)
SubmissionDir = path.Clean(SubmissionDir)
TeamsDir = path.Clean(TeamsDir)
rand.Seed(time.Now().UnixNano())
log.Println("Creating submission directory...")
if _, err := os.Stat(path.Join(SubmissionDir, ".tmp")); os.IsNotExist(err) {
if err := os.MkdirAll(path.Join(SubmissionDir, ".tmp"), 0777); err != nil {
log.Fatal("Unable to create submission directory: ", err)
}
}
log.Println("Opening DB...")
if err := fic.DBInit(*dsn); err != nil {
log.Fatal("Cannot open the database: ", err)
}
defer fic.DBClose()
// Load configuration
settings.LoadAndWatchSettings(path.Join(settings.SettingsDir, settings.SettingsFile), reloadSettings)
log.Println("Registering directory events...")
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
if err := watchsubdir(watcher, SubmissionDir); err != nil {
log.Fatal(err)
}
// Register SIGUSR1 and SIGTERM
interrupt1 := make(chan os.Signal, 1)
signal.Notify(interrupt1, syscall.SIGUSR1)
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, syscall.SIGTERM)
watchedNotify := fsnotify.Create
loop:
for {
select {
case <-interrupt:
break loop
case <-interrupt1:
log.Println("SIGUSR1 received, retreating all files in queue...")
walkAndTreat(SubmissionDir)
log.Println("SIGUSR1 treated.")
case ev := <-watcher.Events:
if d, err := os.Lstat(ev.Name); err == nil && ev.Op&fsnotify.Create == fsnotify.Create && d.Mode().IsDir() && d.Mode()&os.ModeSymlink == 0 && d.Name() != ".tmp" {
// Register new subdirectory
if err := watchsubdir(watcher, ev.Name); err != nil {
log.Println(err)
}
} else if err == nil && ev.Op&watchedNotify == watchedNotify && d.Mode().IsRegular() {
if *debugINotify {
log.Println("Treating event:", ev, "for", ev.Name)
}
go treat(ev.Name)
} else if err == nil && ev.Op&fsnotify.Write == fsnotify.Write {
log.Println("FSNOTIFY WRITE SEEN. Prefer looking at them, as it appears files are not atomically moved.")
watchedNotify = fsnotify.Write
} else if err == nil && *debugINotify {
log.Println("Skipped event:", ev, "for", ev.Name)
}
case err := <-watcher.Errors:
log.Println("error:", err)
}
}
}
func treat(raw_path string) {
// Extract
spath := strings.Split(strings.TrimPrefix(raw_path, SubmissionDir), "/")
if len(spath) == 3 {
if spath[1] == "_registration" {
treatRegistration(raw_path, spath[2])
return
}
var teamid int64
var err error
if teamid, err = strconv.ParseInt(spath[1], 10, 64); err != nil {
if lnk, err := os.Readlink(path.Join(TeamsDir, spath[1])); err != nil {
log.Printf("[ERR] Unable to readlink %q: %s\n", path.Join(TeamsDir, spath[1]), err)
return
} else if teamid, err = strconv.ParseInt(lnk, 10, 64); err != nil {
log.Printf("[ERR] Error during ParseInt team %q: %s\n", lnk, err)
return
}
}
var team *fic.Team
if team, err = fic.GetTeam(teamid); err != nil {
log.Printf("[ERR] Unable to retrieve team %d: %s\n", teamid, err)
return
}
switch spath[2] {
case "name":
treatRename(raw_path, team)
case "issue":
treatIssue(raw_path, team)
case "hint":
treatOpeningHint(raw_path, team)
case "choices":
treatWantChoices(raw_path, team)
case ".locked":
treatLocked(raw_path, team)
default:
treatSubmission(raw_path, team, spath[2])
}
} else {
log.Println("Invalid new file:", raw_path)
}
}

100
checker/registration.go Normal file
View file

@ -0,0 +1,100 @@
package main
import (
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"html"
"io/ioutil"
"log"
"math/rand"
"os"
"path"
"srs.epita.fr/fic-server/libfic"
)
var (
allowRegistration = false
canJoinTeam = false
denyTeamCreation = false
)
type uTeamRegistration struct {
TeamName string
JTeam int64
Members []fic.Member
}
func registrationProcess(id string, team *fic.Team, members []fic.Member, team_id string) {
for _, m := range members {
// Force Id to 0, as it shouldn't have been defined yet
m.Id = 0
if err := team.GainMember(&m); err != nil {
log.Println("[WRN] Unable to add member (", m, ") to team (", team, "):", err)
}
}
teamDirPath := fmt.Sprintf("%d", team.Id)
// Create team directories into TEAMS
if err := os.MkdirAll(path.Join(TeamsDir, teamDirPath), 0777); err != nil {
log.Println(id, "[ERR]", err)
}
if err := os.Symlink(teamDirPath, path.Join(TeamsDir, team_id)); err != nil {
log.Println(id, "[ERR]", err)
}
appendGenQueue(fic.GenStruct{Id: id, Type: fic.GenTeam, TeamId: team.Id})
appendGenQueue(fic.GenStruct{Id: id, Type: fic.GenTeams})
}
func treatRegistration(pathname string, team_id string) {
// Generate a unique identifier to follow the request in logs
bid := make([]byte, 5)
binary.LittleEndian.PutUint32(bid, rand.Uint32())
id := "[" + base64.StdEncoding.EncodeToString(bid) + "]"
log.Println(id, "New registration receive", pathname)
var nTeam uTeamRegistration
if !allowRegistration {
log.Printf("%s [ERR] Registration received, whereas disabled. Skipped.\n", id)
} else if cnt_raw, err := ioutil.ReadFile(pathname); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
} else if err := json.Unmarshal(cnt_raw, &nTeam); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
} else if nTeam.JTeam > 0 {
if !canJoinTeam {
log.Printf("%s [ERR] Join team received, whereas disabled. Skipped.\n", id)
} else if len(nTeam.Members) != 1 {
log.Printf("%s [ERR] Join team received, with incorrect member length: %d. Skipped.\n", id, len(nTeam.Members))
} else if team, err := fic.GetTeam(nTeam.JTeam); err != nil {
log.Printf("%s [ERR] Unable to join registered team %d: %s\n", id, nTeam.JTeam, err)
} else {
registrationProcess(id, team, nTeam.Members, team_id)
if err := os.Remove(pathname); err != nil {
log.Printf("%s [WRN] %s\n", id, err)
}
}
} else if denyTeamCreation {
log.Printf("%s [ERR] Registration received, whereas team creation denied. Skipped.\n", id)
} else if validTeamName(nTeam.TeamName) {
if team, err := fic.CreateTeam(nTeam.TeamName, fic.HSL{H: rand.Float64(), L: 1, S: 0.5}.ToRGB(), ""); err != nil {
log.Printf("%s [ERR] Unable to register new team %s: %s\n", id, nTeam.TeamName, err)
} else {
registrationProcess(id, team, nTeam.Members, team_id)
if err := os.Remove(pathname); err != nil {
log.Printf("%s [WRN] %s\n", id, err)
}
if _, err := fic.NewEvent(fmt.Sprintf("Souhaitons bonne chance à l'équipe <strong>%s</strong> qui vient de nous rejoindre&#160;!", html.EscapeString(team.Name)), "info"); err != nil {
log.Printf("%s [WRN] Unable to create event: %s\n", id, err)
}
appendGenQueue(fic.GenStruct{Id: id, Type: fic.GenEvents})
}
}
}

50
checker/rename.go Normal file
View file

@ -0,0 +1,50 @@
package main
import (
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"html"
"io/ioutil"
"log"
"math/rand"
"os"
"regexp"
"srs.epita.fr/fic-server/libfic"
)
func validTeamName(name string) bool {
match, err := regexp.MatchString("^[A-Za-z0-9 àéèêëîïôùûü_-]{1,32}$", name)
return err == nil && match
}
func treatRename(pathname string, team *fic.Team) {
// Generate a unique identifier to follow the request in logs
bid := make([]byte, 5)
binary.LittleEndian.PutUint32(bid, rand.Uint32())
id := "[" + base64.StdEncoding.EncodeToString(bid) + "]"
log.Println(id, "New renameTeam receive", pathname)
var keys map[string]string
if cnt_raw, err := ioutil.ReadFile(pathname); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
} else if err := json.Unmarshal(cnt_raw, &keys); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
} else if validTeamName(keys["newName"]) {
team.Name = keys["newName"]
if _, err := team.Update(); err != nil {
log.Printf("%s [WRN] Unable to change team name: %s\n", id, err)
}
appendGenQueue(fic.GenStruct{Id: id, Type: fic.GenTeam, TeamId: team.Id})
if _, err := fic.NewEvent(fmt.Sprintf("Souhaitons bonne chance à l'équipe <strong>%s</strong> qui vient de nous rejoindre&#160;!", html.EscapeString(team.Name)), "info"); err != nil {
log.Printf("%s [WRN] Unable to create event: %s\n", id, err)
}
appendGenQueue(fic.GenStruct{Id: id, Type: fic.GenEvents})
if err := os.Remove(pathname); err != nil {
log.Printf("%s [ERR] %s\n", id, err)
}
}
}

173
checker/submission.go Normal file
View file

@ -0,0 +1,173 @@
package main
import (
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"html"
"io/ioutil"
"log"
"math/rand"
"os"
"strconv"
"golang.org/x/crypto/blake2b"
"srs.epita.fr/fic-server/libfic"
)
type ResponsesUpload struct {
Keys map[int]string `json:"flags"`
MCQs map[int]bool `json:"mcqs"`
MCQJ map[int]string `json:"justifications"`
}
func treatSubmission(pathname string, team *fic.Team, exercice_id string) {
// Generate a unique identifier to follow the request in logs
bid := make([]byte, 5)
binary.LittleEndian.PutUint32(bid, rand.Uint32())
id := "[" + base64.StdEncoding.EncodeToString(bid) + "]"
log.Println(id, "New submission receive", pathname)
// Parse exercice_id argument
eid, err := strconv.ParseInt(exercice_id, 10, 64)
if err != nil {
log.Printf("%s [ERR] %s is not a valid number: %s\n", id, exercice_id, err)
return
}
// Identifier should not be blacklisted for the team
if blacklistteam, ok := TeamLockedExercices[team.Id]; ok {
if locked, ok := blacklistteam[exercice_id]; ok && locked {
log.Printf("%s [!!!] Submission received for team's locked exercice %d\n", id, eid)
return
}
}
// Find the given exercice
exercice, err := fic.GetExercice(eid)
if err != nil {
log.Printf("%s [ERR] Unable to find exercice %d: %s\n", id, eid, err)
return
}
// Check the exercice is not disabled
if exercice.Disabled {
log.Println("[!!!] The team submits something for a disabled exercice")
return
}
// Check the team can access this exercice
if !team.HasAccess(exercice) {
log.Println("[!!!] The team submits something for an exercice it doesn't have access yet")
return
}
// Find the corresponding theme
theme, err := fic.GetTheme(exercice.IdTheme)
if err != nil {
log.Printf("%s [ERR] Unable to retrieve theme for exercice %d: %s\n", id, eid, err)
return
}
// Theme should not be locked
if theme.Locked {
log.Printf("%s [!!!] Submission received for locked theme %d (eid=%d): %s\n", id, exercice.IdTheme, eid, theme.Name)
return
}
// Read received file
cnt_raw, err := ioutil.ReadFile(pathname)
if err != nil {
log.Println(id, "[ERR] Unable to read file;", err)
return
}
// Save checksum to treat duplicates
cksum := blake2b.Sum512(cnt_raw)
if err != nil {
log.Println(id, "[ERR] JSON parsing error:", err)
return
}
// Parse it
var responses ResponsesUpload
err = json.Unmarshal(cnt_raw, &responses)
if err != nil {
log.Println(id, "[ERR] JSON parsing error:", err)
return
}
// Ensure the team didn't already solve this exercice
tm := team.HasSolved(exercice)
if tm != nil {
log.Printf("%s [WRN] Team %d ALREADY solved exercice %d (%s : %s), continuing for eventual bonus flags\n", id, team.Id, exercice.Id, theme.Name, exercice.Title)
}
// Handle MCQ justifications: convert to expected keyid
for cid, j := range responses.MCQJ {
if mcq, choice, err := fic.GetMCQbyChoice(cid); err != nil {
log.Println(id, "[ERR] Unable to retrieve mcq from justification:", err)
return
} else if mcq.IdExercice != exercice.Id {
log.Println(id, "[ERR] We retrieve an invalid MCQ: from exercice", mcq.IdExercice, "whereas expected from exercice", exercice.Id)
return
} else if key, err := choice.GetJustifiedFlag(exercice); err != nil {
// Most probably, we enter here because the selected choice has not to be justified
// So, just ignore this case as it will be invalidated by the mcq validation
continue
} else {
if responses.Keys == nil {
responses.Keys = map[int]string{}
}
responses.Keys[key.Id] = j
}
}
// Check given answers
solved, err := exercice.CheckResponse(cksum[:], responses.Keys, responses.MCQs, team)
if err != nil {
log.Println(id, "[ERR] Unable to CheckResponse:", err)
appendGenQueue(fic.GenStruct{Id: id, Type: fic.GenTeam, TeamId: team.Id})
return
}
// At this point, we have treated the file, so it can be safely deleted
if err := os.Remove(pathname); err != nil {
log.Println(id, "[ERR] Can't remove file:", err)
}
if tm != nil {
appendGenQueue(fic.GenStruct{Id: id, Type: fic.GenTeam, TeamId: team.Id})
} else if solved {
log.Printf("%s Team %d SOLVED exercice %d (%s : %s)\n", id, team.Id, exercice.Id, theme.Name, exercice.Title)
if err := exercice.Solved(team); err != nil {
log.Println(id, "[ERR] Unable to mark the challenge as solved:", err)
}
// Write event
if lvl, err := exercice.GetLevel(); err != nil {
log.Println(id, "[ERR] Unable to get exercice level:", err)
} else if _, err := fic.NewEvent(fmt.Sprintf("L'équipe %s a résolu le <strong>%d<sup>e</sup></strong> défi %s&#160;!", html.EscapeString(team.Name), lvl, theme.Name), "success"); err != nil {
log.Println(id, "[WRN] Unable to create event:", err)
}
appendGenQueue(fic.GenStruct{Id: id, Type: fic.GenTeam, TeamId: team.Id})
appendGenQueue(fic.GenStruct{Id: id, Type: fic.GenThemes})
appendGenQueue(fic.GenStruct{Id: id, Type: fic.GenTeams})
} else {
log.Printf("%s Team %d submit an invalid solution for exercice %d (%s : %s)\n", id, team.Id, exercice.Id, theme.Name, exercice.Title)
// Write event (only on first try)
if tm == nil {
if lvl, err := exercice.GetLevel(); err != nil {
log.Println(id, "[ERR] Unable to get exercice level:", err)
} else if _, err := fic.NewEvent(fmt.Sprintf("L'équipe %s tente le <strong>%d<sup>e</sup></strong> défi %s&#160;!", html.EscapeString(team.Name), lvl, theme.Name), "warning"); err != nil {
log.Println(id, "[WRN] Unable to create event:", err)
}
}
appendGenQueue(fic.GenStruct{Id: id, Type: fic.GenTeam, TeamId: team.Id})
}
appendGenQueue(fic.GenStruct{Id: id, Type: fic.GenEvents})
}