diff --git a/frontend/chname.go b/frontend/chname.go index 0b57e672..4d444f7a 100644 --- a/frontend/chname.go +++ b/frontend/chname.go @@ -3,45 +3,19 @@ package main import ( "log" "net/http" - "strings" + "path" ) var denyNameChange bool = true -type ChNameHandler struct {} - -func (n ChNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - +func ChNameHandler(w http.ResponseWriter, r *http.Request, team string, sURL []string) { if denyNameChange { log.Printf("UNHANDELED %s name change request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent()) http.Error(w, "{\"errmsg\":\"Le changement de nom est prohibé.\"}", http.StatusForbidden) - return - } - - log.Printf("Handling %s name change request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent()) - - // Check request type and size - if r.Method != "POST" { - http.Error(w, "{\"errmsg\":\"Requête invalide.\"}", http.StatusBadRequest) - return - } else if r.ContentLength < 0 || r.ContentLength > 1023 { - http.Error(w, "{\"errmsg\":\"Requête trop longue ou de taille inconnue\"}", http.StatusRequestEntityTooLarge) - return - } - - // Extract URL arguments - var sURL = strings.Split(r.URL.Path, "/") - - if len(sURL) != 1 { - http.Error(w, "{\"errmsg\":\"Requête invalide.\"}", http.StatusBadRequest) - return - } - - team := sURL[0] - - // Enqueue file for backend treatment - if saveTeamFile(SubmissionDir, team, "name", w, r) { + } else if len(sURL) != 0 { + http.Error(w, "{\"errmsg\":\"Arguments manquants.\"}", http.StatusBadRequest) + } else if saveTeamFile(path.Join(team, "name"), w, r) { + // File enqueued for backend treatment http.Error(w, "{\"errmsg\":\"Demande de changement de nom acceptée\"}", http.StatusAccepted) } } diff --git a/frontend/hint.go b/frontend/hint.go index de138647..dcf8e0f0 100644 --- a/frontend/hint.go +++ b/frontend/hint.go @@ -1,39 +1,13 @@ package main import ( - "log" "net/http" - "strings" + "path" ) -type HintHandler struct {} - -func (h HintHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - log.Printf("Handling %s opening hint request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent()) - - w.Header().Set("Content-Type", "application/json") - - // Check request type and size - if r.Method != "POST" { - http.Error(w, "{\"errmsg\":\"Requête invalide.\"}", http.StatusBadRequest) - return - } else if r.ContentLength <= 0 || r.ContentLength > 1023 { - http.Error(w, "{\"errmsg\":\"Requête trop longue ou de taille inconnue\"}", http.StatusRequestEntityTooLarge) - return - } - - // Extract URL arguments - var sURL = strings.Split(r.URL.Path, "/") - - if len(sURL) != 1 && len(sURL) != 2 { - http.Error(w, "{\"errmsg\":\"Requête invalide.\"}", http.StatusBadRequest) - return - } - - team := sURL[0] - +func HintHandler(w http.ResponseWriter, r *http.Request, team string, sURL []string) { // Enqueue file for backend treatment - if saveTeamFile(SubmissionDir, team, "hint", w, r) { + if saveTeamFile(path.Join(team, "hint"), w, r) { http.Error(w, "{\"errmsg\":\"Demande d'astuce acceptée...\"}", http.StatusAccepted) } } diff --git a/frontend/main.go b/frontend/main.go index 9cf49af9..817e037f 100644 --- a/frontend/main.go +++ b/frontend/main.go @@ -7,64 +7,15 @@ import ( "net/http" "os" "path" - "time" fronttime "srs.epita.fr/fic-server/frontend/time" "srs.epita.fr/fic-server/settings" ) -const startedFile = "started" - -var TeamsDir string -var SubmissionDir string -var TmpSubmissionDir string - -var touchTimer *time.Timer = nil - -func touchStartedFile() { - if fd, err := os.Create(path.Join(SubmissionDir, startedFile)); err == nil { - log.Println("Started! Go, Go, Go!!") - fd.Close() - } else { - log.Fatal("Unable to start challenge:", err) - } -} - -func reloadSettings(config settings.FICSettings) { - if fronttime.ChallengeStart != config.Start || fronttime.ChallengeEnd != config.End { - if touchTimer != nil { - touchTimer.Stop() - } - startSub := config.Start.Sub(time.Now()) - if startSub > 0 { - log.Println("Challenge will starts at", config.Start, "in", startSub) - - if _, err := os.Stat(path.Join(SubmissionDir, startedFile)); !os.IsNotExist(err) { - os.Remove(path.Join(SubmissionDir, startedFile)) - } - - touchTimer = time.AfterFunc(config.Start.Sub(time.Now().Add(time.Duration(1 * time.Second))), touchStartedFile) - } else { - log.Println("Challenge started at", config.Start, "since", -startSub) - touchStartedFile() - } - log.Println("Challenge ends on", config.End) - - fronttime.ChallengeStart = config.Start - fronttime.ChallengeEnd = config.End - } else { - log.Println("Configuration reloaded, but start/end times doesn't change.") - } - - enableResolutionRoute = config.EnableResolutionRoute - denyNameChange = config.DenyNameChange - allowRegistration = config.AllowRegistration -} - func main() { var bind = flag.String("bind", "127.0.0.1:8080", "Bind port/socket") var prefix = flag.String("prefix", "", "Request path prefix to strip (from proxy)") - flag.StringVar(&TeamsDir, "teams", "./TEAMS", "Base directory where save teams JSON files") + var teamsDir = flag.String("teams", "./TEAMS/", "Base directory where find existing teams") flag.StringVar(&SubmissionDir, "submission", "./submissions/", "Base directory where save submissions") flag.Parse() @@ -81,14 +32,14 @@ func main() { } // Load configuration - settings.LoadAndWatchSettings(path.Join(TeamsDir, settings.SettingsFile), reloadSettings) + settings.LoadAndWatchSettings(path.Join(*teamsDir, settings.SettingsFile), reloadSettings) // Register handlers - http.Handle(fmt.Sprintf("%s/chname/", *prefix), http.StripPrefix(fmt.Sprintf("%s/chname/", *prefix), ChNameHandler{})) - http.Handle(fmt.Sprintf("%s/openhint/", *prefix), http.StripPrefix(fmt.Sprintf("%s/openhint/", *prefix), HintHandler{})) - http.Handle(fmt.Sprintf("%s/registration", *prefix), http.StripPrefix(fmt.Sprintf("%s/registration", *prefix), RegistrationHandler{})) + http.Handle(fmt.Sprintf("%s/chname/", *prefix), http.StripPrefix(fmt.Sprintf("%s/chname/", *prefix), submissionTeamChecker{"name change", ChNameHandler, *teamsDir})) + http.Handle(fmt.Sprintf("%s/openhint/", *prefix), http.StripPrefix(fmt.Sprintf("%s/openhint/", *prefix), submissionTeamChecker{"opening hint", HintHandler, *teamsDir})) + http.Handle(fmt.Sprintf("%s/registration", *prefix), http.StripPrefix(fmt.Sprintf("%s/registration", *prefix), submissionChecker{"registration", RegistrationHandler})) http.Handle(fmt.Sprintf("%s/resolution/", *prefix), http.StripPrefix(fmt.Sprintf("%s/resolution/", *prefix), ResolutionHandler{})) - http.Handle(fmt.Sprintf("%s/submission/", *prefix), http.StripPrefix(fmt.Sprintf("%s/submission/", *prefix), SubmissionHandler{})) + http.Handle(fmt.Sprintf("%s/submission/", *prefix), http.StripPrefix(fmt.Sprintf("%s/submission/", *prefix), submissionTeamChecker{"submission", SubmissionHandler, *teamsDir})) http.Handle(fmt.Sprintf("%s/time.json", *prefix), http.StripPrefix(*prefix, fronttime.TimeHandler{})) // Serve pages diff --git a/frontend/register.go b/frontend/register.go index 3d5ccd19..13abe08e 100644 --- a/frontend/register.go +++ b/frontend/register.go @@ -1,6 +1,7 @@ package main import ( + "io/ioutil" "log" "net/http" "path" @@ -8,19 +9,13 @@ import ( var allowRegistration bool = false -type RegistrationHandler struct {} - -func (e RegistrationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - +func RegistrationHandler(w http.ResponseWriter, r *http.Request, sURL []string) { if !allowRegistration { log.Printf("UNHANDLED %s registration request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent()) http.Error(w, "{\"errmsg\":\"L'enregistrement d'équipe n'est pas permis.\"}", http.StatusForbidden) return } - log.Printf("Handling %s registration request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent()) - // Check request type and size if r.Method != "POST" { http.Error(w, "{\"errmsg\":\"Requête invalide.\"}", http.StatusBadRequest) @@ -30,12 +25,19 @@ func (e RegistrationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - // Enqueue file for backend treatment - if err := saveFile(path.Join(SubmissionDir, "_registration"), "", r); err != nil { - log.Println("Unable to open registration file:", err) + if tmpfile, err := ioutil.TempFile(path.Join(SubmissionDir, "_registration"), ""); err != nil { + log.Println("Unable to generate registration file:", err) http.Error(w, "{\"errmsg\":\"Internal server error. Please retry in few seconds.\"}", http.StatusInternalServerError) - return - } + } else { + // The file will be reopened by saveFile + tmpfile.Close() - http.Error(w, "{\"errmsg\":\"Demande d'enregistrement acceptée\"}", http.StatusAccepted) + if err := saveFile(tmpfile.Name(), r); err != nil { + log.Println("Unable to open registration file:", err) + http.Error(w, "{\"errmsg\":\"Internal server error. Please retry in few seconds.\"}", http.StatusInternalServerError) + } else { + // File enqueued for backend treatment + http.Error(w, "{\"errmsg\":\"Demande d'enregistrement acceptée\"}", http.StatusAccepted) + } + } } diff --git a/frontend/save.go b/frontend/save.go index 78089eb2..9c2e103c 100644 --- a/frontend/save.go +++ b/frontend/save.go @@ -9,34 +9,31 @@ import ( "path" ) -func saveTeamFile(dirname string, team string, filename string, w http.ResponseWriter, r *http.Request) bool { - if _, err := os.Stat(path.Join(dirname, team)); os.IsNotExist(err) || len(team) < 1 || team[0] == '_' { +var SubmissionDir string +var TmpSubmissionDir string + +func saveTeamFile(p string, w http.ResponseWriter, r *http.Request) bool { + if len(SubmissionDir) < 1 || len(p) < 1 { http.Error(w, "{\"errmsg\":\"Requête invalide.\"}", http.StatusBadRequest) return false - } else { - if len(filename) <= 0 { - log.Println("EMPTY $EXERCICE RECEIVED:", filename) - http.Error(w, "{\"errmsg\":\"Internal server error. Please retry in few seconds.\"}", http.StatusInternalServerError) - return false - } - + } else if len(p) <= 0 { + log.Println("EMPTY $EXERCICE RECEIVED:", p) + http.Error(w, "{\"errmsg\":\"Internal server error. Please retry in few seconds.\"}", http.StatusInternalServerError) + return false + } else if _, err := os.Stat(path.Join(SubmissionDir, p)); !os.IsNotExist(err) { // Previous submission not treated - if _, err := os.Stat(path.Join(SubmissionDir, team, filename)); !os.IsNotExist(err) { - http.Error(w, "{\"errmsg\":\"Du calme ! une requête est déjà en cours de traitement.\"}", http.StatusPaymentRequired) - return false - } - - if err := saveFile(path.Join(SubmissionDir, team), filename, r); err != nil { - log.Println("Unable to handle submission file:", err) - http.Error(w, "{\"errmsg\":\"Internal server error. Please retry in few seconds.\"}", http.StatusInternalServerError) - return false - } - - return true + http.Error(w, "{\"errmsg\":\"Du calme ! une requête est déjà en cours de traitement.\"}", http.StatusPaymentRequired) + return false + } else if err := saveFile(path.Join(SubmissionDir, p), r); err != nil { + log.Println("Unable to handle submission file:", err) + http.Error(w, "{\"errmsg\":\"Internal server error. Please retry in few seconds.\"}", http.StatusInternalServerError) + return false } + return true } -func saveFile(dirname string, filename string, r *http.Request) error { +func saveFile(p string, r *http.Request) error { + dirname := path.Dir(p) if _, err := os.Stat(dirname); os.IsNotExist(err) { if err := os.MkdirAll(dirname, 0755); err != nil { return err @@ -57,12 +54,9 @@ func saveFile(dirname string, filename string, r *http.Request) error { writer.Flush() tmpfile.Close() - if filename == "" { - filename = path.Base(tmpfile.Name()) - } - - if err := os.Rename(tmpfile.Name(), path.Join(dirname, filename)); err != nil { + if err := os.Rename(tmpfile.Name(), p); err != nil { log.Println("[ERROR] Unable to move file: ", err) + return err } return nil diff --git a/frontend/settings.go b/frontend/settings.go new file mode 100644 index 00000000..6d2d7154 --- /dev/null +++ b/frontend/settings.go @@ -0,0 +1,55 @@ +package main + +import ( + "log" + "os" + "path" + "time" + + fronttime "srs.epita.fr/fic-server/frontend/time" + "srs.epita.fr/fic-server/settings" +) + +const startedFile = "started" + +var touchTimer *time.Timer = nil + +func touchStartedFile() { + if fd, err := os.Create(path.Join(SubmissionDir, startedFile)); err == nil { + log.Println("Started! Go, Go, Go!!") + fd.Close() + } else { + log.Fatal("Unable to start challenge:", err) + } +} + +func reloadSettings(config settings.FICSettings) { + if fronttime.ChallengeStart != config.Start || fronttime.ChallengeEnd != config.End { + if touchTimer != nil { + touchTimer.Stop() + } + startSub := config.Start.Sub(time.Now()) + if startSub > 0 { + log.Println("Challenge will starts at", config.Start, "in", startSub) + + if _, err := os.Stat(path.Join(SubmissionDir, startedFile)); !os.IsNotExist(err) { + os.Remove(path.Join(SubmissionDir, startedFile)) + } + + touchTimer = time.AfterFunc(config.Start.Sub(time.Now().Add(time.Duration(1 * time.Second))), touchStartedFile) + } else { + log.Println("Challenge started at", config.Start, "since", -startSub) + touchStartedFile() + } + log.Println("Challenge ends on", config.End) + + fronttime.ChallengeStart = config.Start + fronttime.ChallengeEnd = config.End + } else { + log.Println("Configuration reloaded, but start/end times doesn't change.") + } + + enableResolutionRoute = config.EnableResolutionRoute + denyNameChange = config.DenyNameChange + allowRegistration = config.AllowRegistration +} diff --git a/frontend/submissions.go b/frontend/submissions.go new file mode 100644 index 00000000..9ee2a74a --- /dev/null +++ b/frontend/submissions.go @@ -0,0 +1,67 @@ +package main + +import ( + "log" + "net/http" + "os" + "path" + "strings" +) + +type submissionHandler func(w http.ResponseWriter, r *http.Request, sURL []string) + +type submissionChecker struct{ + kind string + next submissionHandler +} + +type submissionTeamHandler func(w http.ResponseWriter, r *http.Request, team string, sURL []string) + +type submissionTeamChecker struct{ + kind string + next submissionTeamHandler + teamsDir string +} + +func (c submissionChecker) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.Printf("Handling %s %s request from %s: %s [%s]\n", r.Method, c.kind, r.RemoteAddr, r.URL.Path, r.UserAgent()) + + w.Header().Set("Content-Type", "application/json") + + // Check request type and size + if r.Method != "POST" { + http.Error(w, "{\"errmsg\":\"Requête invalide.\"}", http.StatusBadRequest) + return + } else if r.ContentLength < 0 || r.ContentLength > 1023 { + http.Error(w, "{\"errmsg\":\"Requête trop longue ou de taille inconnue\"}", http.StatusRequestEntityTooLarge) + return + } + + // Extract URL arguments + var sURL = strings.Split(r.URL.Path, "/") + + c.next(w, r, sURL) +} + +func (c submissionTeamChecker) ServeHTTP(w http.ResponseWriter, r *http.Request) { + submissionChecker{c.kind, func(w http.ResponseWriter, r *http.Request, sURL []string){ + if len(sURL) < 1 { + http.Error(w, "{\"errmsg\":\"Arguments manquants.\"}", http.StatusBadRequest) + return + } + team := sURL[0] + + // Check team validity and existance + if len(team) < 1 || team[0] == '_' { + log.Println("INVALID TEAM:", team) + http.Error(w, "{\"errmsg\":\"Équipe inexistante.\"}", http.StatusBadRequest) + return + } else if _, err := os.Stat(path.Join(c.teamsDir, team)); os.IsNotExist(err) { + log.Println("UNKNOWN TEAM:", team) + http.Error(w, "{\"errmsg\":\"Équipe inexistante.\"}", http.StatusBadRequest) + return + } + + c.next(w, r, team, sURL[1:]) + }}.ServeHTTP(w, r) +} diff --git a/frontend/submit.go b/frontend/submit.go index 201be4a4..1ed64e85 100644 --- a/frontend/submit.go +++ b/frontend/submit.go @@ -2,53 +2,33 @@ package main import ( "fmt" - "log" "net/http" + "path" "strconv" - "strings" "time" fronttime "srs.epita.fr/fic-server/frontend/time" ) -type SubmissionHandler struct {} - -func (s SubmissionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - log.Printf("Handling %s submission request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent()) - - w.Header().Set("Content-Type", "application/json") - - // Check request type and size - if r.Method != "POST" { - http.Error(w, "{\"errmsg\":\"Requête invalide.\"}", http.StatusBadRequest) - return - } else if r.ContentLength < 0 || r.ContentLength > 1023 { - http.Error(w, "{\"errmsg\":\"Requête trop longue ou de taille inconnue\"}", http.StatusRequestEntityTooLarge) - return - } - - // Extract URL arguments - var sURL = strings.Split(r.URL.Path, "/") - - if len(sURL) != 2 { - http.Error(w, "{\"errmsg\":\"Arguments manquants.\"}", http.StatusBadRequest) - return - } - team := sURL[0] - +func SubmissionHandler(w http.ResponseWriter, r *http.Request, team string, sURL []string) { if time.Now().Sub(fronttime.ChallengeEnd) > 0 { http.Error(w, "{\"errmsg\":\"Vous ne pouvez plus soumettre, le challenge est terminé.\"}", http.StatusForbidden) return } - if pex, err := strconv.Atoi(sURL[1]); err != nil { + if len(sURL) != 1 { + http.Error(w, "{\"errmsg\":\"Arguments manquants.\"}", http.StatusBadRequest) + return + } + + // Check exercice validity then save the submission + if pex, err := strconv.Atoi(sURL[0]); err != nil { http.Error(w, "{\"errmsg\":\"Requête invalide.\"}", http.StatusBadRequest) return - } else { - exercice := fmt.Sprintf("%d", pex) - - if saveTeamFile(TeamsDir, team, exercice, w, r) { - http.Error(w, "{\"errmsg\":\"Son traitement est en cours...\"}", http.StatusAccepted) - } + } else if exercice := fmt.Sprintf("%d", pex); len(exercice) < 1 { + http.Error(w, "{\"errmsg\":\"Requête invalide.\"}", http.StatusBadRequest) + return + } else if saveTeamFile(path.Join(team, exercice), w, r) { + http.Error(w, "{\"errmsg\":\"Son traitement est en cours...\"}", http.StatusAccepted) } }