Le challenge forensic vous place dans la peau de spécialistes en investigation numérique. Nous mettons à votre disposition une vingtaine de scénarios différents, dans lesquels vous devrez faire les différentes étapes de la caractérisation d’une réponse à incident proposées.
-Chaque scénario met en scène un contexte d’entreprise, ayant découvert récemment qu’elle a été victime d’une cyberattaque. Elle vous demande alors de l’aider à caractériser, afin de mieux comprendre la situation, notamment le mode opératoire de l’adversaire, les impacts de la cyberattaque, le périmètre technique compromis, etc. Il faudra parfois aussi l’éclairer sur les premières étapes de la réaction.
`, - Rules: `Pendant toute la durée du challenge, vous aurez accès à tous les scénarios, mais seulement à la première des 5 étapes. Chaque étape supplémentaire est débloquée lorsque vous validez l’intégralité de l’étape précédente. Toutefois, pour dynamiser le challenge toutes les étapes et tous les scénarios seront débloquées pour la dernière heure du challenge.
-Nous mettons à votre disposition une plateforme sur laquelle vous pourrez obtenir les informations sur le contexte de l’entreprise et, généralement, une série de fichiers qui semblent appropriés pour avancer dans l’investigation.
-La validation d’une étape se fait sur la plateforme, après avoir analysé les informations fournies, en répondant à des questions plus ou moins précises. Il s’agit le plus souvent des mots-clefs que l’on placerait dans un rapport.
-Pour vous débloquer ou accélérer votre investigation, vous pouvez accéder à quelques indices, en échange d’une décote sur votre score d’un certain nombre de points préalablement affichés.
-Chaque équipe dispose d’un compteur de points dans l’intervalle ]-∞;+∞[ (aux détails techniques près), à partir duquel le classement est établi.
-Vous perdez des points en dévoilant des indices, en demandant des propositions de réponses en remplacement de certains champs de texte, ou en essayant un trop grand nombre de fois une réponse.
-Le nombre de points que vous fait perdre un indice dépend habituellement de l’aide qu’il vous apportera et est indiqué avant de le dévoiler, car il peut fluctuer en fonction de l’avancement du challenge.
-Pour chaque champ de texte, vous disposez de 10 tentatives avant de perdre des points (vous perdez les points même si vous ne validez pas l’étape) pour chaque tentative supplémentaire : -0,25 point entre 11 et 20, -0,5 entre 21 et 30, -0,75 entre 31 et 40, …
-La seule manière de gagner des points est de valider une étape d’un scénario dans son intégralité. Le nombre de points gagnés dépend de la difficulté théorique de l’étape ainsi que d’éventuels bonus. Un bonus de 10 % est accordé à la première équipe qui valide une étape. D’autres bonus peuvent ponctuer le challenge, détaillé dans la partie suivante.
-Le classement est établi par équipe, selon le nombre de points récoltés et perdus par tous les membres. En cas d’égalité au score, les équipes sont départagées en fonction de leur ordre d’arrivée à ce score.
-Le challenge forensic est jalonné de plusieurs temps forts durant lesquels certains calculs détaillés dans la partie précédente peuvent être altérés. L’équipe d’animation du challenge vous avertira environ 15 minutes avant le début de la modification.
-Chaque modification se répercute instantanément dans votre interface, attendez simplement qu’elle apparaisse afin d’être certain d’en bénéficier. Un compte à rebours est généralement affiché sur les écrans pour indiquer la fin d’un temps fort. La fin d’application d’un bonus est déterminé par l’heure d’arrivée de votre demande sur nos serveurs.
-Sans y être limité ou assuré, sachez que durant les précédentes éditions du challenge forensic, nous avons par exemple : doublé les points de défis peu tentés, doublé les points de tous les défis pendant 30 minutes, réduit le coût des indices pendant 15 minutes, etc.
- -Tous les étudiants de la majeure Système, Réseaux et Sécurité de l’ÉPITA, son équipe enseignante ainsi que le commandement de la cyberdéfense vous souhaitent bon courage pour cette nouvelle éditions du challenge !
`, - YourMission: `Vous voici aujourd'hui dans la peau de spécialistes en investigation numérique. Vous avez à votre disposition une vingtaine de scénarios différents dans lesquels vous devrez faire les différentes étapes de la caractérisation d’une réponse à incident.
-Chaque scénario est découpé en 5 grandes étapes de difficulté croissante. Un certain nombre de points est attribué à chaque étape, avec un processus de validation automatique.
-Un classement est établi en temps réel, tenant compte des différents bonus, en fonction du nombre de points de chaque équipe.
`, - }) -} - -func reset(c *gin.Context) { +func reset(_ httprouter.Params, body []byte) (interface{}, error) { var m map[string]string - err := c.ShouldBindJSON(&m) - if err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) - return + if err := json.Unmarshal(body, &m); err != nil { + return nil, err } - t, ok := m["type"] - if !ok { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Field type not found"}) + if t, ok := m["type"]; !ok { + return nil, errors.New("Field type not found") + } else if t == "teams" { + return true, fic.ResetTeams() + } else if t == "challenges" { + return true, fic.ResetExercices() + } else if t == "game" { + return true, fic.ResetGame() + } else { + return nil, errors.New("Unknown reset type") } - - switch t { - case "teams": - err = fic.ResetTeams() - case "challenges": - err = fic.ResetExercices() - case "game": - err = fic.ResetGame() - case "annexes": - err = fic.ResetAnnexes() - case "settings": - err = ResetSettings() - case "challengeInfo": - err = ResetChallengeInfo() - default: - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Unknown reset type"}) - return - } - - if err != nil { - log.Printf("Unable to reset (type=%q): %s", t, err) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to performe the reset: %s", err.Error())}) - return - } - - c.JSON(http.StatusOK, true) } diff --git a/admin/api/sync.go b/admin/api/sync.go deleted file mode 100644 index 582c55e0..00000000 --- a/admin/api/sync.go +++ /dev/null @@ -1,411 +0,0 @@ -package api - -import ( - "fmt" - "log" - "net/http" - "net/url" - "os" - "path" - "reflect" - "strings" - - "srs.epita.fr/fic-server/admin/generation" - "srs.epita.fr/fic-server/admin/sync" - "srs.epita.fr/fic-server/libfic" - - "github.com/gin-gonic/gin" - "go.uber.org/multierr" -) - -var lastSyncError = "" - -func flatifySyncErrors(errs error) (ret []string) { - for _, err := range multierr.Errors(errs) { - ret = append(ret, err.Error()) - } - return -} - -func declareSyncRoutes(router *gin.RouterGroup) { - apiSyncRoutes := router.Group("/sync") - - // Return the global sync status - apiSyncRoutes.GET("/status", func(c *gin.Context) { - syncMtd := "Disabled" - if sync.GlobalImporter != nil { - syncMtd = sync.GlobalImporter.Kind() - } - - var syncId *string - if sync.GlobalImporter != nil { - syncId = sync.GlobalImporter.Id() - } - - c.JSON(http.StatusOK, gin.H{ - "sync-type": reflect.TypeOf(sync.GlobalImporter).Name(), - "sync-id": syncId, - "sync": syncMtd, - "pullMutex": !sync.OneGitPullStatus(), - "syncMutex": !sync.OneDeepSyncStatus() && !sync.OneThemeDeepSyncStatus(), - "progress": sync.DeepSyncProgress, - "lastError": lastSyncError, - }) - }) - - // Base sync checks if the local directory is in sync with remote one. - apiSyncRoutes.POST("/base", func(c *gin.Context) { - err := sync.GlobalImporter.Sync() - if err != nil { - lastSyncError = err.Error() - c.JSON(http.StatusExpectationFailed, gin.H{"errmsg": err.Error()}) - } else { - lastSyncError = "" - c.JSON(http.StatusOK, true) - } - }) - - // Speedy sync performs a recursive synchronization without importing files. - apiSyncRoutes.POST("/speed", func(c *gin.Context) { - st := sync.SpeedySyncDeep(sync.GlobalImporter) - sync.EditDeepReport(&st, false) - c.JSON(http.StatusOK, st) - }) - - // Deep sync: a fully recursive synchronization (can be limited by theme). - apiSyncRoutes.POST("/deep", func(c *gin.Context) { - r := sync.SyncDeep(sync.GlobalImporter) - lastSyncError = "" - c.JSON(http.StatusOK, r) - }) - - apiSyncRoutes.POST("/local-diff", APIDiffDBWithRemote) - - apiSyncDeepRoutes := apiSyncRoutes.Group("/deep/:thid") - apiSyncDeepRoutes.Use(ThemeHandler) - // Special route to handle standalone exercices - apiSyncRoutes.POST("/deep/0", func(c *gin.Context) { - var st []string - for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, &fic.StandaloneExercicesTheme, 0, 250, nil)) { - st = append(st, se.Error()) - } - sync.EditDeepReport(&sync.SyncReport{Exercices: st}, false) - sync.DeepSyncProgress = 255 - lastSyncError = "" - c.JSON(http.StatusOK, st) - }) - apiSyncDeepRoutes.POST("", func(c *gin.Context) { - theme := c.MustGet("theme").(*fic.Theme) - - exceptions := sync.LoadThemeException(sync.GlobalImporter, theme) - - var st []string - for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, theme, 0, 250, exceptions)) { - st = append(st, se.Error()) - } - sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theme.Name: st}}, false) - sync.DeepSyncProgress = 255 - lastSyncError = "" - c.JSON(http.StatusOK, st) - }) - - // Auto sync: to use with continuous deployment, in a development env - apiSyncRoutes.POST("/auto/*p", autoSync) - - // Themes - apiSyncRoutes.POST("/fixurlids", fixAllURLIds) - - apiSyncRoutes.POST("/themes", func(c *gin.Context) { - _, errs := sync.SyncThemes(sync.GlobalImporter) - lastSyncError = "" - c.JSON(http.StatusOK, flatifySyncErrors(errs)) - }) - - apiSyncThemesRoutes := apiSyncRoutes.Group("/themes/:thid") - apiSyncThemesRoutes.Use(ThemeHandler) - apiSyncThemesRoutes.POST("/fixurlid", func(c *gin.Context) { - theme := c.MustGet("theme").(*fic.Theme) - if theme.FixURLId() { - v, err := theme.Update() - if err != nil { - log.Println("Unable to UpdateTheme after fixurlid:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when saving the theme."}) - return - } - - c.JSON(http.StatusOK, v) - } else { - c.AbortWithStatusJSON(http.StatusOK, 0) - } - }) - - // Exercices - declareSyncExercicesRoutes(apiSyncRoutes) - declareSyncExercicesRoutes(apiSyncThemesRoutes) - - // Videos sync imports resolution.mp4 from path stored in database. - apiSyncRoutes.POST("/videos", func(c *gin.Context) { - exercices, err := fic.GetExercices() - if err != nil { - log.Println("Unable to GetExercices:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve exercices list."}) - return - } - - for _, e := range exercices { - if len(e.VideoURI) == 0 || !strings.HasPrefix(e.VideoURI, "$RFILES$/") { - continue - } - - vpath, err := url.PathUnescape(strings.TrimPrefix(e.VideoURI, "$RFILES$/")) - if err != nil { - c.JSON(http.StatusExpectationFailed, gin.H{"errmsg": fmt.Sprintf("Unable to perform URL unescape: %s", err.Error())}) - return - } - - _, err = sync.ImportFile(sync.GlobalImporter, vpath, func(filePath, URI string) (interface{}, error) { - e.VideoURI = path.Join("$FILES$", strings.TrimPrefix(filePath, fic.FilesDir)) - return e.Update() - }) - if err != nil { - c.JSON(http.StatusExpectationFailed, gin.H{"errmsg": err.Error()}) - return - } - } - - c.JSON(http.StatusOK, true) - }) - - // Remove soluces from the database. - apiSyncRoutes.POST("/drop_soluces", func(c *gin.Context) { - exercices, err := fic.GetExercices() - if err != nil { - log.Println("Unable to GetExercices:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve exercices list."}) - return - } - - var errs error - for _, e := range exercices { - // Remove any published video - if len(e.VideoURI) > 0 && strings.HasPrefix(e.VideoURI, "$FILES$") { - vpath := path.Join(fic.FilesDir, strings.TrimPrefix(e.VideoURI, "$FILES$/")) - err = os.Remove(vpath) - if err != nil { - errs = multierr.Append(errs, fmt.Errorf("unable to delete published video (%q): %w", e.VideoURI, err)) - } - } - - // Clean the database - if len(e.VideoURI) > 0 || len(e.Resolution) > 0 { - e.VideoURI = "" - e.Resolution = "" - - _, err = e.Update() - if err != nil { - errs = multierr.Append(errs, fmt.Errorf("unable to update exercice (%d: %s): %w", e.Id, e.Title, err)) - } - } - } - - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"errmsg": flatifySyncErrors(err)}) - } else { - c.JSON(http.StatusOK, true) - } - }) -} - -func declareSyncExercicesRoutes(router *gin.RouterGroup) { - router.POST("/exercices", func(c *gin.Context) { - theme := c.MustGet("theme").(*fic.Theme) - exceptions := sync.LoadThemeException(sync.GlobalImporter, theme) - - _, errs := sync.SyncExercices(sync.GlobalImporter, theme, exceptions) - c.JSON(http.StatusOK, flatifySyncErrors(errs)) - }) - apiSyncExercicesRoutes := router.Group("/exercices/:eid") - apiSyncExercicesRoutes.Use(ExerciceHandler) - apiSyncExercicesRoutes.POST("", func(c *gin.Context) { - theme := c.MustGet("theme").(*fic.Theme) - exercice := c.MustGet("exercice").(*fic.Exercice) - - exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil) - - _, _, _, errs := sync.SyncExercice(sync.GlobalImporter, theme, exercice.Path, nil, exceptions) - c.JSON(http.StatusOK, flatifySyncErrors(errs)) - }) - apiSyncExercicesRoutes.POST("/files", func(c *gin.Context) { - exercice := c.MustGet("exercice").(*fic.Exercice) - theme := c.MustGet("theme").(*fic.Theme) - - exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil) - - c.JSON(http.StatusOK, flatifySyncErrors(sync.ImportExerciceFiles(sync.GlobalImporter, exercice, exceptions))) - }) - apiSyncExercicesRoutes.POST("/fixurlid", func(c *gin.Context) { - exercice := c.MustGet("exercice").(*fic.Exercice) - if exercice.FixURLId() { - v, err := exercice.Update() - if err != nil { - log.Println("Unable to UpdateExercice after fixurlid:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when saving the exercice."}) - return - } - - c.JSON(http.StatusOK, v) - } else { - c.AbortWithStatusJSON(http.StatusOK, 0) - } - }) - apiSyncExercicesRoutes.POST("/hints", func(c *gin.Context) { - exercice := c.MustGet("exercice").(*fic.Exercice) - theme := c.MustGet("theme").(*fic.Theme) - - exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil) - - _, errs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice), exceptions) - c.JSON(http.StatusOK, flatifySyncErrors(errs)) - }) - apiSyncExercicesRoutes.POST("/flags", func(c *gin.Context) { - exercice := c.MustGet("exercice").(*fic.Exercice) - theme := c.MustGet("theme").(*fic.Theme) - - exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil) - _, errs := sync.SyncExerciceFlags(sync.GlobalImporter, exercice, exceptions) - _, herrs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice), exceptions) - c.JSON(http.StatusOK, flatifySyncErrors(multierr.Append(errs, herrs))) - }) -} - -// autoSync tries to performs a smart synchronization, when in development environment. -// It'll sync most of modified things, and will delete out of sync data. -// Avoid using it in a production environment. -func autoSync(c *gin.Context) { - p := strings.Split(strings.TrimPrefix(c.Params.ByName("p"), "/"), "/") - - if !IsProductionEnv { - if err := sync.GlobalImporter.Sync(); err != nil { - lastSyncError = err.Error() - log.Println("Unable to sync.GI.Sync:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to perform the pull."}) - return - } - lastSyncError = "" - } - - themes, err := fic.GetThemes() - if err != nil { - log.Println("Unable to GetThemes:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve theme list."}) - return - } - - // No argument, do a deep sync - if len(p) == 0 { - if !IsProductionEnv { - for _, theme := range themes { - theme.DeleteDeep() - } - } - - st := sync.SyncDeep(sync.GlobalImporter) - c.JSON(http.StatusOK, st) - return - } - - var theTheme *fic.Theme - - // Find the given theme - for _, theme := range themes { - if theme.Path == p[0] { - theTheme = theme - break - } - } - - if theTheme == nil { - // The theme doesn't exists locally, perhaps it has not been imported already? - rThemes, err := sync.GetThemes(sync.GlobalImporter) - if err == nil { - for _, theme := range rThemes { - if theme == p[0] { - sync.SyncThemes(sync.GlobalImporter) - - themes, err := fic.GetThemes() - if err == nil { - for _, theme := range themes { - if theme.Path == p[0] { - theTheme = theme - break - } - } - } - - break - } - } - } - - if theTheme == nil { - c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("Theme not found %q", p[0])}) - return - } - } - - if !IsProductionEnv { - exercices, err := theTheme.GetExercices() - if err == nil { - for _, exercice := range exercices { - if len(p) <= 1 || exercice.Path == path.Join(p[0], p[1]) { - exercice.DeleteDeep() - } - } - } - } - - exceptions := sync.LoadThemeException(sync.GlobalImporter, theTheme) - - var st []string - for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, theTheme, 0, 250, exceptions)) { - st = append(st, se.Error()) - } - sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theTheme.Name: st}}, false) - sync.DeepSyncProgress = 255 - - resp, err := generation.FullGeneration() - if err == nil { - defer resp.Body.Close() - } - - c.JSON(http.StatusOK, st) -} - -func diffDBWithRemote() (map[string][]syncDiff, error) { - diffs := map[string][]syncDiff{} - - themes, err := fic.GetThemesExtended() - if err != nil { - return nil, err - } - - // Compare inner themes - for _, theme := range themes { - diffs[theme.Name], err = diffThemeWithRemote(theme) - if err != nil { - return nil, fmt.Errorf("Unable to diffThemeWithRemote: %w", err) - } - } - - return diffs, err -} - -func APIDiffDBWithRemote(c *gin.Context) { - diffs, err := diffDBWithRemote() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) - return - } - - c.JSON(http.StatusOK, diffs) -} diff --git a/admin/api/team.go b/admin/api/team.go index d5d21992..0755468b 100644 --- a/admin/api/team.go +++ b/admin/api/team.go @@ -3,639 +3,199 @@ package api import ( "encoding/json" "fmt" - "log" - "net/http" - "strconv" "strings" "time" - "srs.epita.fr/fic-server/admin/pki" "srs.epita.fr/fic-server/libfic" - "github.com/gin-gonic/gin" + "github.com/julienschmidt/httprouter" ) -func declareTeamsRoutes(router *gin.RouterGroup) { - router.GET("/teams.json", func(c *gin.Context) { - teams, err := fic.ExportTeams(false) - if err != nil { - log.Println("Unable to ExportTeams:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during teams export."}) - return - } +func init() { + router.GET("/api/teams.json", apiHandler( + func(httprouter.Params, []byte) (interface{}, error) { + return fic.ExportTeams() + })) + router.GET("/api/teams-binding", apiHandler( + func(httprouter.Params, []byte) (interface{}, error) { + return bindingTeams() + })) + router.GET("/api/teams-nginx-members", apiHandler( + func(httprouter.Params, []byte) (interface{}, error) { + return nginxGenMember() + })) + router.GET("/api/teams-tries.json", apiHandler( + func(httprouter.Params, []byte) (interface{}, error) { + return fic.GetTries(nil, nil) + })) - c.JSON(http.StatusOK, teams) - }) - router.GET("/teams-members.json", func(c *gin.Context) { - teams, err := fic.ExportTeams(true) - if err != nil { - log.Println("Unable to ExportTeams:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during teams export."}) - return - } + router.GET("/api/teams/", apiHandler( + func(httprouter.Params, []byte) (interface{}, error) { + return fic.GetTeams() + })) + router.POST("/api/teams/", apiHandler(createTeam)) - c.JSON(http.StatusOK, teams) - }) - router.GET("/teams-associations.json", allAssociations) - router.GET("/teams-binding", bindingTeams) - router.GET("/teams-nginx", nginxGenTeams) - router.POST("/refine_colors", refineTeamsColors) - router.POST("/disableinactiveteams", disableInactiveTeams) - router.POST("/enableallteams", enableAllTeams) - router.GET("/teams-members-nginx", nginxGenMember) - router.GET("/teams-tries.json", func(c *gin.Context) { - tries, err := fic.GetTries(nil, nil) - if err != nil { - log.Println("Unable to GetTries:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieves tries."}) - return - } - - c.JSON(http.StatusOK, tries) - }) - - router.GET("/teams", func(c *gin.Context) { - teams, err := fic.GetTeams() - if err != nil { - log.Println("Unable to GetTeams:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during teams listing."}) - return - } - - c.JSON(http.StatusOK, teams) - }) - router.POST("/teams", createTeam) - - apiTeamsRoutes := router.Group("/teams/:tid") - apiTeamsRoutes.Use(TeamHandler) - apiTeamsRoutes.GET("/", func(c *gin.Context) { - c.JSON(http.StatusOK, c.MustGet("team").(*fic.Team)) - }) - apiTeamsRoutes.PUT("/", updateTeam) - apiTeamsRoutes.POST("/", addTeamMember) - apiTeamsRoutes.DELETE("/", deleteTeam) - apiTeamsRoutes.GET("/score-grid.json", func(c *gin.Context) { - team := c.MustGet("team").(*fic.Team) - - sg, err := team.ScoreGrid() - if err != nil { - log.Printf("Unable to get ScoreGrid(tid=%d): %s", team.Id, err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during score grid calculation."}) - return - } - - c.JSON(http.StatusOK, sg) - }) - - apiTeamsPublicRoutes := router.Group("/teams/:tid") - apiTeamsPublicRoutes.Use(TeamPublicHandler) - apiTeamsPublicRoutes.GET("/my.json", func(c *gin.Context) { - var team *fic.Team - if t, ok := c.Get("team"); ok && t != nil { - team = t.(*fic.Team) - } - tfile, err := fic.MyJSONTeam(team, true) - if err != nil { - log.Println("Unable to get MyJSONTeam:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team JSON generation."}) - return - } - - c.JSON(http.StatusOK, tfile) - }) - apiTeamsPublicRoutes.GET("/wait.json", func(c *gin.Context) { - var team *fic.Team - if t, ok := c.Get("team"); ok && t != nil { - team = t.(*fic.Team) - } - tfile, err := fic.MyJSONTeam(team, false) - if err != nil { - log.Println("Unable to get MyJSONTeam:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team JSON generation."}) - return - } - - c.JSON(http.StatusOK, tfile) - }) - apiTeamsPublicRoutes.GET("/stats.json", func(c *gin.Context) { - var team *fic.Team - if t, ok := c.Get("team"); ok && t != nil { - team = t.(*fic.Team) - } - if team != nil { - stats, err := team.GetStats() - if err != nil { - log.Println("Unable to get GetStats:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during stats calculation."}) - return + router.GET("/api/teams/:tid/", apiHandler(teamHandler( + func(team fic.Team, _ []byte) (interface{}, error) { + return team, nil + }))) + router.PUT("/api/teams/:tid/", apiHandler(teamHandler(updateTeam))) + router.POST("/api/teams/:tid/", apiHandler(teamHandler(addTeamMember))) + router.DELETE("/api/teams/:tid/", apiHandler(teamHandler( + func(team fic.Team, _ []byte) (interface{}, error) { + return team.Delete() + }))) + router.GET("/api/teams/:tid/my.json", apiHandler(teamPublicHandler( + func(team *fic.Team, _ []byte) (interface{}, error) { + return fic.MyJSONTeam(team, true) + }))) + router.GET("/api/teams/:tid/wait.json", apiHandler(teamPublicHandler( + func(team *fic.Team, _ []byte) (interface{}, error) { + return fic.MyJSONTeam(team, false) + }))) + router.GET("/api/teams/:tid/stats.json", apiHandler(teamPublicHandler( + func(team *fic.Team, _ []byte) (interface{}, error) { + if team != nil { + return team.GetStats() + } else { + return fic.GetTeamsStats(nil) } - - c.JSON(http.StatusOK, stats) - } else { - stats, err := fic.GetTeamsStats(nil) - if err != nil { - log.Println("Unable to get GetTeamsStats:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during global stats calculation."}) - return + }))) + router.GET("/api/teams/:tid/history.json", apiHandler(teamPublicHandler( + func(team *fic.Team, _ []byte) (interface{}, error) { + if team != nil { + return team.GetHistory() + } else { + return fic.GetTeamsStats(nil) } - - c.JSON(http.StatusOK, stats) - } - }) - apiTeamsRoutes.GET("/history.json", func(c *gin.Context) { - team := c.MustGet("team").(*fic.Team) - - history, err := team.GetHistory() - if err != nil { - log.Println("Unable to get GetHistory:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during history calculation."}) - return - } - - c.JSON(http.StatusOK, history) - }) - apiTeamsRoutes.PATCH("/history.json", updateHistory) - apiTeamsRoutes.DELETE("/history.json", delHistory) - apiTeamsPublicRoutes.GET("/tries", func(c *gin.Context) { - team := c.MustGet("team").(*fic.Team) - - tries, err := fic.GetTries(team, nil) - if err != nil { - log.Println("Unable to GetTries:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during tries calculation."}) - return - } - - c.JSON(http.StatusOK, tries) - }) - apiTeamsRoutes.GET("/members", func(c *gin.Context) { - team := c.MustGet("team").(*fic.Team) - - members, err := team.GetMembers() - if err != nil { - log.Println("Unable to GetMembers:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during members retrieval."}) - return - } - - c.JSON(http.StatusOK, members) - }) - apiTeamsRoutes.POST("/members", addTeamMember) - apiTeamsRoutes.PUT("/members", setTeamMember) - - declareTeamsPasswordRoutes(apiTeamsRoutes) - declareTeamClaimsRoutes(apiTeamsRoutes) - declareTeamCertificateRoutes(apiTeamsRoutes) - - // Import teams from cyberrange - router.POST("/cyberrange-teams.json", importTeamsFromCyberrange) + }))) + router.DELETE("/api/teams/:tid/history.json", apiHandler(teamPublicHandler(delHistory))) + router.GET("/api/teams/:tid/tries", apiHandler(teamPublicHandler( + func(team *fic.Team, _ []byte) (interface{}, error) { + return fic.GetTries(team, nil) + }))) + router.GET("/api/teams/:tid/members", apiHandler(teamHandler( + func(team fic.Team, _ []byte) (interface{}, error) { + return team.GetMembers() + }))) + router.POST("/api/teams/:tid/members", apiHandler(teamHandler(addTeamMember))) + router.PUT("/api/teams/:tid/members", apiHandler(teamHandler(setTeamMember))) } -func TeamHandler(c *gin.Context) { - tid, err := strconv.ParseInt(string(c.Params.ByName("tid")), 10, 64) - if err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid team identifier"}) - return - } - - team, err := fic.GetTeam(tid) - if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Team not found"}) - return - } - - c.Set("team", team) - - c.Next() -} - -func TeamPublicHandler(c *gin.Context) { - tid, err := strconv.ParseInt(string(c.Params.ByName("tid")), 10, 64) - if err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid team identifier"}) - return - } - - if tid != 0 { - team, err := fic.GetTeam(tid) - if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Team not found"}) - return - } - - c.Set("team", team) +func nginxGenMember() (string, error) { + if teams, err := fic.GetTeams(); err != nil { + return "", err } else { - c.Set("team", nil) - } - - c.Next() -} - -func nginxGenTeams(c *gin.Context) { - teams, err := fic.GetTeams() - if err != nil { - log.Println("Unable to GetTeams:", err.Error()) - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - ret := "" - for _, team := range teams { - ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%d\"; }\n", strings.ToLower(team.Name), team.Id) - } - - c.String(http.StatusOK, ret) -} - -func nginxGenMember(c *gin.Context) { - teams, err := fic.GetTeams() - if err != nil { - log.Println("Unable to GetTeams:", err.Error()) - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - ret := "" - for _, team := range teams { - if members, err := team.GetMembers(); err == nil { - for _, member := range members { - ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%d\"; }\n", member.Nickname, team.Id) - } - } else { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - } - - c.String(http.StatusOK, ret) -} - -func bindingTeams(c *gin.Context) { - teams, err := fic.GetTeams() - if err != nil { - log.Println("Unable to GetTeams:", err.Error()) - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - ret := "" - for _, team := range teams { - if members, err := team.GetMembers(); err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } else { - var mbs []string - for _, member := range members { - mbs = append(mbs, fmt.Sprintf("%s %s", member.Firstname, member.Lastname)) - } - ret += fmt.Sprintf("%d;%s;%s\n", team.Id, team.Name, strings.Join(mbs, ";")) - } - } - - c.String(http.StatusOK, ret) -} - -type teamAssociation struct { - Association string `json:"association"` - TeamId int64 `json:"team_id"` -} - -func allAssociations(c *gin.Context) { - teams, err := fic.GetTeams() - if err != nil { - log.Println("Unable to GetTeams:", err.Error()) - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - var ret []teamAssociation - - for _, team := range teams { - assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - for _, a := range assocs { - ret = append(ret, teamAssociation{a, team.Id}) - } - } - - c.JSON(http.StatusOK, ret) -} - -func importTeamsFromCyberrange(c *gin.Context) { - file, err := c.FormFile("file") - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"errmsg": "Failed to get file: " + err.Error()}) - return - } - - src, err := file.Open() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"errmsg": "Failed to open file: " + err.Error()}) - return - } - defer src.Close() - - var ut []fic.CyberrangeTeamBase - err = json.NewDecoder(src).Decode(&fic.CyberrangeAPIResponse{Data: &ut}) - if err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) - return - } - - teams, err := fic.GetTeams() - if err != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Impossible de récupérer la liste des équipes actuelles: %s", err.Error())}) - return - } - - for _, crteam := range ut { - var exist_team *fic.Team + ret := "" for _, team := range teams { - if team.Name == crteam.Name || team.ExternalId == crteam.UUID { - exist_team = team - break - } - } - - if exist_team != nil { - exist_team.Name = crteam.Name - exist_team.ExternalId = crteam.UUID - _, err = exist_team.Update() - } else { - exist_team, err = fic.CreateTeam(crteam.Name, fic.RandomColor().ToRGB(), crteam.UUID) - } - - if err != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Impossible d'ajouter/de modifier l'équipe %v: %s", crteam, err.Error())}) - return - } - - // Import members - if c.DefaultQuery("nomembers", "0") != "" && len(crteam.Members) > 0 { - exist_team.ClearMembers() - - for _, member := range crteam.Members { - _, err = exist_team.AddMember(member.Name, "", member.Nickname, exist_team.Name) - if err != nil { - log.Printf("Unable to add member %q to team %s (tid=%d): %s", member.UUID, exist_team.Name, exist_team.Id, err.Error()) + if members, err := team.GetMembers(); err == nil { + for _, member := range members { + ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%d\"; }\n", member.Nickname, team.Id) } + } else { + return "", err } } - } - teams, err = fic.GetTeams() - if err != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Impossible de récupérer la liste des équipes après import: %s", err.Error())}) - return + return ret, nil } - - c.JSON(http.StatusOK, teams) } -func createTeam(c *gin.Context) { - var ut fic.Team - err := c.ShouldBindJSON(&ut) - if err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) - return +func bindingTeams() (string, error) { + if teams, err := fic.GetTeams(); err != nil { + return "", err + } else { + ret := "" + for _, team := range teams { + if members, err := team.GetMembers(); err != nil { + return "", err + } else { + var mbs []string + for _, member := range members { + mbs = append(mbs, fmt.Sprintf("%s %s", member.Firstname, member.Lastname)) + } + ret += fmt.Sprintf("%d;%s;%s\n", team.Id, team.Name, strings.Join(mbs, ";")) + } + } + return ret, nil } - - if ut.Color == 0 { - ut.Color = fic.RandomColor().ToRGB() - } - - team, err := fic.CreateTeam(strings.TrimSpace(ut.Name), ut.Color, ut.ExternalId) - if err != nil { - log.Println("Unable to CreateTeam:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team creation."}) - return - } - - c.JSON(http.StatusOK, team) } -func updateTeam(c *gin.Context) { - team := c.MustGet("team").(*fic.Team) +type uploadedTeam struct { + Name string + Color uint32 +} +type uploadedMember struct { + Firstname string + Lastname string + Nickname string + Company string +} + +func createTeam(_ httprouter.Params, body []byte) (interface{}, error) { + var ut uploadedTeam + if err := json.Unmarshal(body, &ut); err != nil { + return nil, err + } + + return fic.CreateTeam(strings.TrimSpace(ut.Name), ut.Color) +} + +func updateTeam(team fic.Team, body []byte) (interface{}, error) { var ut fic.Team - err := c.ShouldBindJSON(&ut) - if err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) - return + if err := json.Unmarshal(body, &ut); err != nil { + return nil, err } ut.Id = team.Id - if ut.Password != nil && *ut.Password == "" { - ut.Password = nil + if _, err := ut.Update(); err != nil { + return nil, err } - _, err = ut.Update() - if err != nil { - log.Println("Unable to updateTeam:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team updating."}) - return - } - - c.JSON(http.StatusOK, ut) + return ut, nil } -func refineTeamsColors(c *gin.Context) { - teams, err := fic.GetTeams() - if err != nil { - log.Println("Unable to GetTeams:", err.Error()) - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - for i, team := range teams { - team.Color = fic.HSL{ - H: float64(i)/float64(len(teams)) - 0.2, - S: float64(1) / float64(1+i%2), - L: 0.25 + float64(0.5)/float64(1+i%3), - }.ToRGB() - - _, err = team.Update() - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - } - - c.JSON(http.StatusOK, teams) -} - -func disableInactiveTeams(c *gin.Context) { - teams, err := fic.GetTeams() - if err != nil { - log.Println("Unable to GetTeams:", err.Error()) - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - for _, team := range teams { - var serials []uint64 - serials, err = pki.GetTeamSerials(TeamsDir, team.Id) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - var assocs []string - assocs, err = pki.GetTeamAssociations(TeamsDir, team.Id) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - if len(serials) == 0 && len(assocs) == 0 { - if team.Active { - team.Active = false - team.Update() - } - } else if !team.Active { - team.Active = true - team.Update() - } - } - - c.JSON(http.StatusOK, true) -} - -func enableAllTeams(c *gin.Context) { - teams, err := fic.GetTeams() - if err != nil { - log.Println("Unable to GetTeams:", err.Error()) - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - for _, team := range teams { - if !team.Active { - team.Active = true - team.Update() - } - } - - c.JSON(http.StatusOK, true) -} - -func deleteTeam(c *gin.Context) { - team := c.MustGet("team").(*fic.Team) - - assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id) - if err != nil { - log.Printf("Unable to GetTeamAssociations(tid=%d): %s", team.Id, err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to retrieve team association."}) - return - } - - for _, assoc := range assocs { - err = pki.DeleteTeamAssociation(TeamsDir, assoc) - if err != nil { - log.Printf("Unable to DeleteTeamAssociation(assoc=%s): %s", assoc, err.Error()) - return - } - } - - _, err = team.Delete() - if err != nil { - log.Println("Unable to deleteTeam:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team deletion."}) - return - } - - c.JSON(http.StatusOK, true) -} - -func addTeamMember(c *gin.Context) { - team := c.MustGet("team").(*fic.Team) - - var members []fic.Member - err := c.ShouldBindJSON(&members) - if err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) - return +func addTeamMember(team fic.Team, body []byte) (interface{}, error) { + var members []uploadedMember + if err := json.Unmarshal(body, &members); err != nil { + return nil, err } for _, member := range members { - _, err := team.AddMember(strings.TrimSpace(member.Firstname), strings.TrimSpace(member.Lastname), strings.TrimSpace(member.Nickname), strings.TrimSpace(member.Company)) - if err != nil { - log.Println("Unable to AddMember:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during member creation."}) - return - } + team.AddMember(strings.TrimSpace(member.Firstname), strings.TrimSpace(member.Lastname), strings.TrimSpace(member.Nickname), strings.TrimSpace(member.Company)) } - mmbrs, err := team.GetMembers() - if err != nil { - log.Println("Unable to retrieve members list:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve members list."}) - return - } - - c.JSON(http.StatusOK, mmbrs) + return team.GetMembers() } -func setTeamMember(c *gin.Context) { - team := c.MustGet("team").(*fic.Team) +func setTeamMember(team fic.Team, body []byte) (interface{}, error) { + var members []uploadedMember + if err := json.Unmarshal(body, &members); err != nil { + return nil, err + } + team.ClearMembers() - addTeamMember(c) + for _, member := range members { + team.AddMember(strings.TrimSpace(member.Firstname), strings.TrimSpace(member.Lastname), strings.TrimSpace(member.Nickname), strings.TrimSpace(member.Company)) + } + + return team.GetMembers() } type uploadedHistory struct { - Kind string - Time time.Time - Primary *int64 - Secondary *int64 - Coefficient float32 + Kind string + Time time.Time + Primary *int64 + Secondary *int64 } -func updateHistory(c *gin.Context) { - team := c.MustGet("team").(*fic.Team) - +func delHistory(team *fic.Team, body []byte) (interface{}, error) { var uh uploadedHistory - err := c.ShouldBindJSON(&uh) - if err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) - return + if err := json.Unmarshal(body, &uh); err != nil { + return nil, err } - var givenId int64 - if uh.Secondary != nil { - givenId = *uh.Secondary - } else if uh.Primary != nil { - givenId = *uh.Primary - } - - _, err = team.UpdateHistoryCoeff(uh.Kind, uh.Time, givenId, uh.Coefficient) - if err != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to update this history line: %s", err.Error())}) - return - } - - c.JSON(http.StatusOK, true) -} - -func delHistory(c *gin.Context) { - team := c.MustGet("team").(*fic.Team) - - var uh uploadedHistory - err := c.ShouldBindJSON(&uh) - if err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) - return - } - - _, err = team.DelHistoryItem(uh.Kind, uh.Time, uh.Primary, uh.Secondary) - if err != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to delete this history line: %s", err.Error())}) - return - } - - c.JSON(http.StatusOK, true) + return team.DelHistoryItem(uh.Kind, uh.Time, uh.Primary, uh.Secondary) } diff --git a/admin/api/theme.go b/admin/api/theme.go index c1057397..8ad60fae 100644 --- a/admin/api/theme.go +++ b/admin/api/theme.go @@ -1,95 +1,89 @@ package api import ( + "encoding/json" + "errors" "fmt" - "log" - "net/http" - "path" - "reflect" "strconv" - "strings" "srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/libfic" - "srs.epita.fr/fic-server/settings" - "github.com/gin-gonic/gin" + "github.com/julienschmidt/httprouter" ) -func declareThemesRoutes(router *gin.RouterGroup) { - router.GET("/themes", listThemes) - router.POST("/themes", createTheme) - router.GET("/themes.json", exportThemes) - router.GET("/session-forensic.yaml", func(c *gin.Context) { - if s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil { - log.Printf("Unable to ReadSettings: %s", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during settings reading."}) - return +func init() { + router.GET("/api/themes", apiHandler(listThemes)) + router.POST("/api/themes", apiHandler(createTheme)) + router.GET("/api/themes.json", apiHandler(exportThemes)) + router.GET("/api/files-bindings", apiHandler(bindingFiles)) - } else if challengeinfo, err := sync.GetFileContent(sync.GlobalImporter, "challenge.json"); err != nil { - log.Println("Unable to retrieve challenge.json:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to retrive challenge.json: %s", err.Error())}) - return - } else if ch, err := settings.ReadChallengeInfo(challengeinfo); err != nil { - log.Printf("Unable to ReadChallengeInfo: %s", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during challenge info reading."}) - return - } else if sf, err := fic.GenZQDSSessionFile(ch, s); err != nil { - log.Printf("Unable to GenZQDSSessionFile: %s", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during session file generation."}) - return - } else { - c.JSON(http.StatusOK, sf) - } - }) - router.GET("/files-bindings", bindingFiles) + router.GET("/api/themes/:thid", apiHandler(themeHandler(showTheme))) + router.PUT("/api/themes/:thid", apiHandler(themeHandler(updateTheme))) + router.DELETE("/api/themes/:thid", apiHandler(themeHandler(deleteTheme))) - apiThemesRoutes := router.Group("/themes/:thid") - apiThemesRoutes.Use(ThemeHandler) - apiThemesRoutes.GET("", showTheme) - apiThemesRoutes.PUT("", updateTheme) - apiThemesRoutes.DELETE("", deleteTheme) + router.GET("/api/themes/:thid/exercices", apiHandler(themeHandler(listThemedExercices))) + router.POST("/api/themes/:thid/exercices", apiHandler(themeHandler(createExercice))) - apiThemesRoutes.POST("/diff-sync", APIDiffThemeWithRemote) + router.GET("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(showExercice))) + router.PUT("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(updateExercice))) + router.DELETE("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(deleteExercice))) - apiThemesRoutes.GET("/exercices_stats.json", getThemedExercicesStats) + router.GET("/api/themes/:thid/exercices/:eid/files", apiHandler(exerciceHandler(listExerciceFiles))) + router.POST("/api/themes/:thid/exercices/:eid/files", apiHandler(exerciceHandler(createExerciceFile))) - declareExercicesRoutes(apiThemesRoutes) + router.GET("/api/themes/:thid/exercices/:eid/hints", apiHandler(exerciceHandler(listExerciceHints))) + router.POST("/api/themes/:thid/exercices/:eid/hints", apiHandler(exerciceHandler(createExerciceHint))) + + router.GET("/api/themes/:thid/exercices/:eid/keys", apiHandler(exerciceHandler(listExerciceKeys))) + router.POST("/api/themes/:thid/exercices/:eid/keys", apiHandler(exerciceHandler(createExerciceKey))) // Remote - router.GET("/remote/themes", sync.ApiListRemoteThemes) - router.GET("/remote/themes/:thid", sync.ApiGetRemoteTheme) - router.GET("/remote/themes/:thid/exercices", sync.ApiListRemoteExercices) + router.GET("/api/remote/themes", apiHandler(sync.ApiListRemoteThemes)) + router.GET("/api/remote/themes/:thid", apiHandler(sync.ApiGetRemoteTheme)) + router.GET("/api/remote/themes/:thid/exercices", apiHandler(themeHandler(sync.ApiListRemoteExercices))) + + // Synchronize + router.POST("/api/sync/deep", apiHandler( + func(_ httprouter.Params, _ []byte) (interface{}, error) { + return sync.SyncDeep(sync.GlobalImporter), nil + })) + router.POST("/api/sync/themes", apiHandler( + func(_ httprouter.Params, _ []byte) (interface{}, error) { + return sync.SyncThemes(sync.GlobalImporter), nil + })) + router.POST("/api/sync/themes/:thid/exercices", apiHandler(themeHandler( + func(theme fic.Theme, _ []byte) (interface{}, error) { + return sync.SyncExercices(sync.GlobalImporter, theme), nil + }))) + router.POST("/api/sync/themes/:thid/exercices/:eid/files", apiHandler(exerciceHandler( + func(exercice fic.Exercice, _ []byte) (interface{}, error) { + return sync.SyncExerciceFiles(sync.GlobalImporter, exercice), nil + }))) + router.POST("/api/sync/themes/:thid/exercices/:eid/hints", apiHandler(exerciceHandler( + func(exercice fic.Exercice, _ []byte) (interface{}, error) { + return sync.SyncExerciceHints(sync.GlobalImporter, exercice), nil + }))) + router.POST("/api/sync/themes/:thid/exercices/:eid/keys", apiHandler(exerciceHandler( + func(exercice fic.Exercice, _ []byte) (interface{}, error) { + return sync.SyncExerciceKeys(sync.GlobalImporter, exercice), nil + }))) + router.POST("/api/sync/themes/:thid/exercices/:eid/quiz", apiHandler(exerciceHandler( + func(exercice fic.Exercice, _ []byte) (interface{}, error) { + return sync.SyncExerciceMCQ(sync.GlobalImporter, exercice), nil + }))) + + router.POST("/api/sync/themes/:thid/fixurlid", apiHandler(themeHandler( + func(theme fic.Theme, _ []byte) (interface{}, error) { + if theme.FixURLId() { + return theme.Update() + } + return 0, nil + }))) + router.POST("/api/sync/fixurlids", apiHandler(fixAllURLIds)) } -type Theme struct { - *fic.Theme - ForgeLink string `json:"forge_link,omitempty"` -} - -func ThemeHandler(c *gin.Context) { - thid, err := strconv.ParseInt(string(c.Params.ByName("thid")), 10, 64) - if err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid theme identifier"}) - return - } - - if thid == 0 { - c.Set("theme", &fic.StandaloneExercicesTheme) - } else { - theme, err := fic.GetTheme(thid) - if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Theme not found"}) - return - } - - c.Set("theme", theme) - } - - c.Next() -} - -func fixAllURLIds(c *gin.Context) { +func fixAllURLIds(_ httprouter.Params, _ []byte) (interface{}, error) { nbFix := 0 if themes, err := fic.GetThemes(); err == nil { for _, theme := range themes { @@ -109,268 +103,92 @@ func fixAllURLIds(c *gin.Context) { } } - c.JSON(http.StatusOK, nbFix) + return nbFix, nil } -func bindingFiles(c *gin.Context) { - files, err := fic.GetFiles() - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - ret := "" - for _, file := range files { - ret += fmt.Sprintf("%s;%s\n", file.GetOrigin(), file.Path) - } - - c.String(http.StatusOK, ret) -} - -func listThemes(c *gin.Context) { - themes, err := fic.GetThemes() - if err != nil { - log.Println("Unable to listThemes:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to list themes."}) - return - } - - if has, _ := fic.HasStandaloneExercice(); has { - themes = append([]*fic.Theme{&fic.StandaloneExercicesTheme}, themes...) - } - - c.JSON(http.StatusOK, themes) -} - -func exportThemes(c *gin.Context) { - themes, err := fic.ExportThemes() - if err != nil { - log.Println("Unable to exportthemes:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to export themes."}) - return - } - - c.JSON(http.StatusOK, themes) -} - -func showTheme(c *gin.Context) { - theme := c.MustGet("theme").(*fic.Theme) - - var forgelink string - if fli, ok := sync.GlobalImporter.(sync.ForgeLinkedImporter); ok { - if u, _ := fli.GetThemeLink(theme); u != nil { - forgelink = u.String() +func bindingFiles(_ httprouter.Params, body []byte) (interface{}, error) { + if files, err := fic.GetFiles(); err != nil { + return "", err + } else { + ret := "" + for _, file := range files { + ret += fmt.Sprintf("%s;%s\n", file.GetOrigin(), file.Path) } + return ret, nil } - - c.JSON(http.StatusOK, Theme{theme, forgelink}) } -func createTheme(c *gin.Context) { - var ut fic.Theme - err := c.ShouldBindJSON(&ut) - if err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) - return +func getExercice(args []string) (fic.Exercice, error) { + if tid, err := strconv.Atoi(string(args[0])); err != nil { + return fic.Exercice{}, err + } else if theme, err := fic.GetTheme(tid); err != nil { + return fic.Exercice{}, err + } else if eid, err := strconv.Atoi(string(args[1])); err != nil { + return fic.Exercice{}, err + } else { + return theme.GetExercice(eid) + } +} + +func listThemes(_ httprouter.Params, _ []byte) (interface{}, error) { + return fic.GetThemes() +} + +func exportThemes(_ httprouter.Params, _ []byte) (interface{}, error) { + return fic.ExportThemes() +} + +func showTheme(theme fic.Theme, _ []byte) (interface{}, error) { + return theme, nil +} + +func listThemedExercices(theme fic.Theme, _ []byte) (interface{}, error) { + return theme.GetExercices() +} + +func showThemedExercice(theme fic.Theme, exercice fic.Exercice, body []byte) (interface{}, error) { + return exercice, nil +} + +type uploadedTheme struct { + Name string + URLId string + Authors string + Intro string +} + +func createTheme(_ httprouter.Params, body []byte) (interface{}, error) { + var ut uploadedTheme + if err := json.Unmarshal(body, &ut); err != nil { + return nil, err } if len(ut.Name) == 0 { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Theme's name not filled"}) - return + return nil, errors.New("Theme's name not filled") } - th, err := fic.CreateTheme(&ut) - if err != nil { - log.Println("Unable to createTheme:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during theme creation."}) - return - } - - c.JSON(http.StatusOK, th) + return fic.CreateTheme(ut.Name, ut.URLId, ut.Authors, ut.Intro) } -func updateTheme(c *gin.Context) { - theme := c.MustGet("theme").(*fic.Theme) - +func updateTheme(theme fic.Theme, body []byte) (interface{}, error) { var ut fic.Theme - err := c.ShouldBindJSON(&ut) - if err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) - return + if err := json.Unmarshal(body, &ut); err != nil { + return nil, err } ut.Id = theme.Id if len(ut.Name) == 0 { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Theme's name not filled"}) - return + return nil, errors.New("Theme's name not filled") } if _, err := ut.Update(); err != nil { - log.Println("Unable to updateTheme:", err.Error()) - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during theme update."}) - return - } - - if theme.Locked != ut.Locked { - exercices, err := theme.GetExercices() - if err != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) - return - } - for _, exercice := range exercices { - if exercice.Disabled != ut.Locked { - exercice.Disabled = ut.Locked - _, err = exercice.Update() - if err != nil { - log.Println("Unable to enable/disable exercice: ", exercice.Id, err.Error()) - } - } - } - } - - c.JSON(http.StatusOK, ut) -} - -func deleteTheme(c *gin.Context) { - theme := c.MustGet("theme").(*fic.Theme) - - _, err := theme.Delete() - if err != nil { - log.Println("Unable to deleteTheme:", err.Error()) - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during theme deletion."}) - return - } - - c.JSON(http.StatusOK, true) -} - -func getThemedExercicesStats(c *gin.Context) { - theme := c.MustGet("theme").(*fic.Theme) - - exercices, err := theme.GetExercices() - if err != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to fetch exercices: %s", err.Error())}) - return - } - - ret := []exerciceStats{} - for _, e := range exercices { - ret = append(ret, exerciceStats{ - IdExercice: e.Id, - TeamTries: e.TriedTeamCount(), - TotalTries: e.TriedCount(), - SolvedCount: e.SolvedCount(), - FlagSolved: e.FlagSolved(), - MCQSolved: e.MCQSolved(), - }) - } - c.JSON(http.StatusOK, ret) -} - -func diffThemeWithRemote(theme *fic.Theme) ([]syncDiff, error) { - var diffs []syncDiff - - // Compare theme attributes - theme_remote, err := sync.GetRemoteTheme(theme.Path) - if err != nil { return nil, err + } else { + return ut, nil } - - for _, field := range reflect.VisibleFields(reflect.TypeOf(*theme)) { - if ((field.Name == "Image") && path.Base(reflect.ValueOf(*theme_remote).FieldByName(field.Name).String()) != path.Base(reflect.ValueOf(*theme).FieldByName(field.Name).String())) || (field.Name != "Image" && !reflect.ValueOf(*theme_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*theme).FieldByName(field.Name))) { - if !field.IsExported() || field.Name == "Id" || field.Name == "IdTheme" || field.Name == "IssueKind" || field.Name == "BackgroundColor" { - continue - } - - diffs = append(diffs, syncDiff{ - Field: field.Name, - Link: fmt.Sprintf("themes/%d", theme.Id), - Before: reflect.ValueOf(*theme).FieldByName(field.Name).Interface(), - After: reflect.ValueOf(*theme_remote).FieldByName(field.Name).Interface(), - }) - } - } - - // Compare exercices list - exercices, err := theme.GetExercices() - if err != nil { - return nil, fmt.Errorf("Unable to GetExercices: %w", err) - } - - exercices_remote, err := sync.ListRemoteExercices(theme.Path) - if err != nil { - return nil, fmt.Errorf("Unable to ListRemoteExercices: %w", err) - } - - var not_found []string - var extra_found []string - - for _, exercice_remote := range exercices_remote { - found := false - for _, exercice := range exercices { - if exercice.Path[strings.Index(exercice.Path, "/")+1:] == exercice_remote { - found = true - break - } - } - - if !found { - not_found = append(not_found, exercice_remote) - } - } - - for _, exercice := range exercices { - found := false - for _, exercice_remote := range exercices_remote { - if exercice.Path[strings.Index(exercice.Path, "/")+1:] == exercice_remote { - found = true - break - } - } - - if !found { - extra_found = append(extra_found, exercice.Path[strings.Index(exercice.Path, "/")+1:]) - } - } - - if len(not_found) > 0 || len(extra_found) > 0 { - diffs = append(diffs, syncDiff{ - Field: "theme.Exercices", - Link: fmt.Sprintf("themes/%d", theme.Id), - Before: strings.Join(extra_found, ", "), - After: strings.Join(not_found, ", "), - }) - } - - // Compare inner exercices - for i, exercice := range exercices { - exdiffs, err := diffExerciceWithRemote(exercice, theme) - if err != nil { - return nil, fmt.Errorf("Unable to diffExerciceWithRemote: %w", err) - } - - for _, exdiff := range exdiffs { - if theme.Id == 0 { - exdiff.Field = fmt.Sprintf("exercices[%d].%s", exercice.Id, exdiff.Field) - } else { - exdiff.Field = fmt.Sprintf("exercices[%d].%s", i, exdiff.Field) - } - diffs = append(diffs, exdiff) - } - } - - return diffs, err } -func APIDiffThemeWithRemote(c *gin.Context) { - theme := c.MustGet("theme").(*fic.Theme) - - diffs, err := diffThemeWithRemote(theme) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) - return - } - - c.JSON(http.StatusOK, diffs) +func deleteTheme(theme fic.Theme, _ []byte) (interface{}, error) { + return theme.Delete() } diff --git a/admin/api/version.go b/admin/api/version.go index 52cb0726..a65f9c85 100644 --- a/admin/api/version.go +++ b/admin/api/version.go @@ -1,15 +1,13 @@ package api import ( - "net/http" - - "github.com/gin-gonic/gin" + "github.com/julienschmidt/httprouter" ) -func DeclareVersionRoutes(router *gin.RouterGroup) { - router.GET("/version", showVersion) +func init() { + router.GET("/api/version", apiHandler(showVersion)) } -func showVersion(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"version": 1.0}) +func showVersion(_ httprouter.Params, body []byte) (interface{}, error) { + return map[string]interface{}{"version": 0.5}, nil } diff --git a/admin/app.go b/admin/app.go deleted file mode 100644 index 1ba1910e..00000000 --- a/admin/app.go +++ /dev/null @@ -1,81 +0,0 @@ -package main - -import ( - "context" - "log" - "net/http" - "path/filepath" - "strings" - "time" - - "github.com/gin-gonic/gin" - - "srs.epita.fr/fic-server/admin/api" - "srs.epita.fr/fic-server/settings" -) - -type App struct { - router *gin.Engine - srv *http.Server - cfg *settings.Settings - bind string -} - -func NewApp(cfg *settings.Settings, baseURL string, bind string) App { - if !cfg.WorkInProgress { - gin.SetMode(gin.ReleaseMode) - } - gin.ForceConsoleColor() - router := gin.Default() - - api.DeclareRoutes(router.Group("")) - - var baserouter *gin.RouterGroup - if len(baseURL) > 0 { - router.GET("/", func(c *gin.Context) { - c.Redirect(http.StatusFound, baseURL) - }) - router.GET(filepath.Dir(baseURL)+"/files/*_", func(c *gin.Context) { - path := c.Request.URL.Path - c.Redirect(http.StatusFound, filepath.Join(baseURL, strings.TrimPrefix(path, filepath.Dir(baseURL)))) - }) - - baserouter = router.Group(baseURL) - - api.DeclareRoutes(baserouter) - declareStaticRoutes(baserouter, cfg, baseURL) - } else { - declareStaticRoutes(router.Group(""), cfg, "") - } - - app := App{ - router: router, - bind: bind, - } - - return app -} - -func (app *App) Start() { - app.srv = &http.Server{ - Addr: app.bind, - Handler: app.router, - ReadHeaderTimeout: 15 * time.Second, - ReadTimeout: 15 * time.Second, - WriteTimeout: 10 * time.Second, - IdleTimeout: 30 * time.Second, - } - - log.Printf("Ready, listening on %s\n", app.bind) - if err := app.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("listen: %s\n", err) - } -} - -func (app *App) Stop() { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := app.srv.Shutdown(ctx); err != nil { - log.Fatal("Server Shutdown:", err) - } -} diff --git a/admin/fill_exercices.sh b/admin/fill_exercices.sh new file mode 100755 index 00000000..e2ce2141 --- /dev/null +++ b/admin/fill_exercices.sh @@ -0,0 +1,262 @@ +#!/bin/bash + +BASEURL="http://localhost:8081" +BASEURI="https://owncloud.srs.epita.fr/remote.php/webdav/FIC 2018" +BASEFILE="/mnt/fic/" +CLOUDPASS="$CLOUD_USER:$CLOUD_PASS" + +new_theme() { + NAME=`echo $1 | sed 's/"/\\\\"/g'` + AUTHORS=`echo $2 | sed 's/"/\\\\"/g'` + curl -f -s -d "{\"name\": \"$NAME\", \"authors\": \"$AUTHORS\"}" "${BASEURL}/api/themes" | + grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+" +} + +new_exercice() { + THEME="$1" + TITLE=`echo "$2" | sed 's/"/\\\\"/g'` + STATEMENT=`echo "$3" | sed 's/"/\\\\"/g' | sed ':a;N;$!ba;s/\n/