Split backend service into checker and generator
Both are linked through a unix socket.
This commit is contained in:
parent
f755d7c998
commit
ed091e761c
34 changed files with 660 additions and 208 deletions
1
checker/.gitignore
vendored
Normal file
1
checker/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
checker
|
||||
53
checker/choices.go
Normal file
53
checker/choices.go
Normal 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
52
checker/generation.go
Normal 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
65
checker/hint.go
Normal 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 !", 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
90
checker/issue.go
Normal 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
30
checker/locked.go
Normal 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
221
checker/main.go
Normal 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
100
checker/registration.go
Normal 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 !", 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
50
checker/rename.go
Normal 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 !", 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
173
checker/submission.go
Normal 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 !", 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 !", 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})
|
||||
}
|
||||
Reference in a new issue