Distribute and handle challenge.json

This commit is contained in:
nemunaire 2022-05-01 22:33:59 +02:00
parent e8f6a03cd9
commit dff4f4eb63
20 changed files with 167 additions and 48 deletions

View File

@ -6,6 +6,7 @@ WORKDIR /go/src/srs.epita.fr/fic-server/
COPY go.mod go.sum ./ COPY go.mod go.sum ./
COPY libfic ./libfic/ COPY libfic ./libfic/
COPY settings ./settings/
COPY remote/scores-sync-zqds ./remote/scores-sync-zqds/ COPY remote/scores-sync-zqds ./remote/scores-sync-zqds/
RUN go get -d -v ./remote/scores-sync-zqds && \ RUN go get -d -v ./remote/scores-sync-zqds && \

View File

@ -16,6 +16,9 @@ import (
var IsProductionEnv = false var IsProductionEnv = false
func init() { func init() {
router.GET("/api/challenge.json", apiHandler(getChallengeInfo))
router.PUT("/api/challenge.json", apiHandler(saveChallengeInfo))
router.GET("/api/settings-ro.json", apiHandler(getROSettings)) router.GET("/api/settings-ro.json", apiHandler(getROSettings))
router.GET("/api/settings.json", apiHandler(getSettings)) router.GET("/api/settings.json", apiHandler(getSettings))
router.PUT("/api/settings.json", apiHandler(saveSettings)) router.PUT("/api/settings.json", apiHandler(saveSettings))
@ -44,6 +47,23 @@ func getROSettings(_ httprouter.Params, body []byte) (interface{}, error) {
}, nil }, nil
} }
func getChallengeInfo(_ httprouter.Params, body []byte) (interface{}, error) {
return settings.ReadChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile))
}
func saveChallengeInfo(_ httprouter.Params, body []byte) (interface{}, error) {
var info *settings.ChallengeInfo
if err := json.Unmarshal(body, &info); err != nil {
return nil, err
}
if err := settings.SaveChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile), info); err != nil {
return nil, err
} else {
return info, err
}
}
func getSettings(_ httprouter.Params, body []byte) (interface{}, error) { func getSettings(_ httprouter.Params, body []byte) (interface{}, error) {
if s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil { if s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil {
return nil, err return nil, err
@ -83,9 +103,7 @@ func ApplySettings(config *settings.Settings) {
} }
func ResetSettings() error { func ResetSettings() error {
return settings.SaveSettings(path.Join(settings.SettingsDir, settings.SettingsFile), &settings.FICSettings{ return settings.SaveSettings(path.Join(settings.SettingsDir, settings.SettingsFile), &settings.Settings{
Title: "Challenge FIC",
Authors: "Laboratoire SRS, ÉPITA",
WorkInProgress: IsProductionEnv, WorkInProgress: IsProductionEnv,
FirstBlood: fic.FirstBlood, FirstBlood: fic.FirstBlood,
SubmissionCostBase: fic.SubmissionCostBase, SubmissionCostBase: fic.SubmissionCostBase,

View File

@ -22,8 +22,10 @@ func init() {
router.GET("/api/session-forensic.yaml", apiHandler(func(_ httprouter.Params, _ []byte) (interface{}, error) { router.GET("/api/session-forensic.yaml", apiHandler(func(_ httprouter.Params, _ []byte) (interface{}, error) {
if s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil { if s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil {
return nil, err return nil, err
} else if c, err := settings.ReadChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile)); err != nil {
return nil, err
} else { } else {
return fic.GenZQDSSessionFile(s) return fic.GenZQDSSessionFile(c, s)
} }
})) }))
router.GET("/api/files-bindings", apiHandler(bindingFiles)) router.GET("/api/files-bindings", apiHandler(bindingFiles))

View File

@ -6,27 +6,6 @@
<input type="hidden" class="form-control form-control-sm" id="lastRegeneration" ng-model="config.generation"> <input type="hidden" class="form-control form-control-sm" id="lastRegeneration" ng-model="config.generation">
<div class="form-group row">
<label for="challengeName" class="col-sm-3 col-form-label col-form-label-sm">Nom du challenge</label>
<div class="col-sm-9">
<input type="text" class="form-control form-control-sm" id="challengeName" ng-model="config.title">
</div>
</div>
<div class="form-group row">
<label for="challengeAuthors" class="col-sm-3 col-form-label col-form-label-sm">Auteurs du challenge</label>
<div class="col-sm-9">
<input type="text" class="form-control form-control-sm" id="challengeAuthors" ng-model="config.authors">
</div>
</div>
<div class="form-group row">
<label for="challengeVideo" class="col-sm-3 col-form-label col-form-label-sm">Lien vidéos de résolution</label>
<div class="col-sm-9">
<input type="text" class="form-control form-control-sm" id="challengeVideo" ng-model="config.videoslink">
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label for="startTime" class="col-sm-3 col-form-label col-form-label-sm">Début du challenge</label> <label for="startTime" class="col-sm-3 col-form-label col-form-label-sm">Début du challenge</label>
<div class="col-sm-9"> <div class="col-sm-9">

View File

@ -165,6 +165,12 @@ server {
expires epoch; expires epoch;
add_header Cache-Control no-cache; add_header Cache-Control no-cache;
} }
location = /challenge.json {
root /srv/SETTINGSDIST/;
expires epoch;
add_header X-FIC-time $msec;
add_header Cache-Control no-cache;
}
location = /settings.json { location = /settings.json {
root /srv/SETTINGSDIST/; root /srv/SETTINGSDIST/;
expires epoch; expires epoch;

View File

@ -146,6 +146,12 @@ server {
expires epoch; expires epoch;
add_header Cache-Control no-cache; add_header Cache-Control no-cache;
} }
location /challenge.json {
root ${PATH_SETTINGS}/;
expires epoch;
add_header X-FIC-time $msec;
add_header Cache-Control no-cache;
}
location /settings.json { location /settings.json {
root ${PATH_SETTINGS}/; root ${PATH_SETTINGS}/;
expires epoch; expires epoch;

View File

@ -155,6 +155,12 @@ server {
expires epoch; expires epoch;
add_header Cache-Control no-cache; add_header Cache-Control no-cache;
} }
location = /challenge.json {
root /srv/SETTINGSDIST/;
expires epoch;
add_header X-FIC-time $msec;
add_header Cache-Control no-cache;
}
location = /settings.json { location = /settings.json {
root /srv/SETTINGSDIST/; root /srv/SETTINGSDIST/;
expires epoch; expires epoch;

View File

@ -107,6 +107,14 @@ func init() {
fwd_request(w, r, "http://127.0.0.1:8081/") fwd_request(w, r, "http://127.0.0.1:8081/")
} }
}) })
api.Router().GET("/challenge.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
w.Header().Set("Cache-Control", "no-cache")
if forwarder != nil {
fwd_request(w, r, *forwarder)
} else {
http.ServeFile(w, r, path.Join(settings.SettingsDir, settings.ChallengeFile))
}
})
api.Router().GET("/settings.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { api.Router().GET("/settings.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Cache-Control", "no-cache")
if forwarder != nil { if forwarder != nil {

View File

@ -37,6 +37,15 @@ func reloadSettings(config *settings.Settings) {
log.Println("Unable to move new settings file:", err) log.Println("Unable to move new settings file:", err)
} }
// Copy the challenge info file for distribution
if data, err := ioutil.ReadFile(path.Join(settings.SettingsDir, settings.ChallengeFile)); err != nil {
log.Println("Unable to read challenge info file:", err)
} else if err = ioutil.WriteFile(path.Join(SettingsDistDir, settings.ChallengeFile+".tmp"), data, 0644); err != nil {
log.Println("Unable to write tmp challenge info file:", err)
} else if err := os.Rename(path.Join(SettingsDistDir, settings.ChallengeFile+".tmp"), path.Join(SettingsDistDir, settings.ChallengeFile)); err != nil {
log.Println("Unable to move new challenge info file:", err)
}
if challengeStart != config.Start || challengeEnd != config.End { if challengeStart != config.Start || challengeEnd != config.End {
if touchTimer != nil { if touchTimer != nil {
touchTimer.Stop() touchTimer.Stop()

View File

@ -5,7 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico"> <link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
<meta name="author" content="EPITA Laboratoire SRS">
<meta name="robots" content="all"> <meta name="robots" content="all">
<base href="/"> <base href="/">
%svelte.head% %svelte.head%

View File

@ -4,6 +4,7 @@
Icon, Icon,
} from 'sveltestrap'; } from 'sveltestrap';
import { challengeInfo } from '../stores/challengeinfo.js';
import { settings, time } from '../stores/settings.js'; import { settings, time } from '../stores/settings.js';
</script> </script>
@ -53,9 +54,9 @@
Classement Classement
</a> </a>
<a <a
href="{$settings.videoslink}" href="{$challengeInfo.videoslink}"
class="btn btn-light" class="btn btn-light"
class:disabled={$settings.videoslink === ''} class:disabled={$challengeInfo.videoslink === ''}
> >
<Icon name="laptop-fill" /> <Icon name="laptop-fill" />
Vidéos Vidéos

View File

@ -42,6 +42,7 @@
import ExerciceVideo from '../../components/ExerciceVideo.svelte'; import ExerciceVideo from '../../components/ExerciceVideo.svelte';
import ThemeNav from '../../components/ThemeNav.svelte'; import ThemeNav from '../../components/ThemeNav.svelte';
import { challengeInfo } from '../../stores/challengeinfo.js';
import { my } from '../../stores/my.js'; import { my } from '../../stores/my.js';
import { settings } from '../../stores/settings.js'; import { settings } from '../../stores/settings.js';
@ -54,7 +55,7 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{exercice?exercice.title+" - ":""}{$settings.title}</title> <title>{exercice?exercice.title+" - ":""}{$challengeInfo.title}</title>
</svelte:head> </svelte:head>
{#if exercice} {#if exercice}

View File

@ -30,13 +30,13 @@
Container, Container,
} from 'sveltestrap'; } from 'sveltestrap';
import { settings } from '../../stores/settings.js'; import { challengeInfo } from '../../stores/challengeinfo.js';
export let theme; export let theme;
</script> </script>
<svelte:head> <svelte:head>
<title>{theme?theme.name:""} - {$settings.title}</title> <title>{theme?theme.name:""} - {$challengeInfo.title}</title>
</svelte:head> </svelte:head>
{#if theme} {#if theme}

View File

@ -1,4 +1,5 @@
<script context="module"> <script context="module">
import { challengeInfo } from '../stores/challengeinfo.js';
import { issuesStore } from '../stores/issues.js'; import { issuesStore } from '../stores/issues.js';
import { my } from '../stores/my.js'; import { my } from '../stores/my.js';
import { teamsStore } from '../stores/teams.js'; import { teamsStore } from '../stores/teams.js';
@ -41,6 +42,10 @@
settings.update(await fetch('settings.json', {headers: {'Accept': 'application/json'}}), cb); settings.update(await fetch('settings.json', {headers: {'Accept': 'application/json'}}), cb);
} }
async function refresh_challengeInfo(cb=null) {
challengeInfo.update(await fetch('challenge.json', {headers: {'Accept': 'application/json'}}), cb);
}
let refresh_interval_teams = null; let refresh_interval_teams = null;
async function refresh_teams(cb=null, interval=null) { async function refresh_teams(cb=null, interval=null) {
if (refresh_interval_teams) if (refresh_interval_teams)
@ -102,6 +107,7 @@
} }
export async function load({ stuff }) { export async function load({ stuff }) {
await refresh_challengeInfo();
await refresh_settings(); await refresh_settings();
await refresh_themes(); await refresh_themes();
refresh_teams(); refresh_teams();
@ -115,6 +121,7 @@
return { return {
stuff: { stuff: {
...stuff, ...stuff,
refresh_challengeInfo,
refresh_settings, refresh_settings,
refresh_teams, refresh_teams,
refresh_themes, refresh_themes,
@ -138,7 +145,8 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{$settings.title}</title> <title>{$challengeInfo.title}</title>
<meta name="author" content="{$challengeInfo.authors}">
</svelte:head> </svelte:head>
<!--Styles /--> <!--Styles /-->

View File

@ -12,7 +12,7 @@
import { my } from '../stores/my.js'; import { my } from '../stores/my.js';
import { rank } from '../stores/teams.js'; import { rank } from '../stores/teams.js';
import { settings } from '../stores/settings.js'; import { challengeInfo } from '../stores/challengeinfo.js';
import CardTheme from '../components/CardTheme.svelte'; import CardTheme from '../components/CardTheme.svelte';
@ -21,7 +21,7 @@
<Container fluid class="my-3"> <Container fluid class="my-3">
<h1 class="text-dark"> <h1 class="text-dark">
{$settings.title} {$challengeInfo.title}
<small class="text-muted">Classement</small> <small class="text-muted">Classement</small>
</h1> </h1>
<div class="card niceborder text-light"> <div class="card niceborder text-light">

View File

@ -6,12 +6,13 @@
Icon, Icon,
} from 'sveltestrap'; } from 'sveltestrap';
import { challengeInfo } from '../stores/challengeinfo.js';
import { settings } from '../stores/settings.js'; import { settings } from '../stores/settings.js';
</script> </script>
<Container class="my-3"> <Container class="my-3">
<h1 class="text-dark"> <h1 class="text-dark">
{$settings.title} {$challengeInfo.title}
<small class="text-muted">Règles générales</small> <small class="text-muted">Règles générales</small>
</h1> </h1>

View File

@ -0,0 +1,22 @@
import { readable, writable } from 'svelte/store';
function createChallengeStore() {
const { subscribe, set, update } = writable({});
return {
subscribe,
update: (res_challenge, cb) => {
if (res_challenge.status === 200) {
res_challenge.json().then((challenge) => {
update((s) => (Object.assign({}, challenge)));
if (cb) {
cb(challenge);
}
});
}
},
}
}
export const challengeInfo = createChallengeStore();

View File

@ -20,7 +20,7 @@ type ChallengeZQDS struct {
type SessionZQDS struct { type SessionZQDS struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Scenario string `json:"scenario"` Scenario string `json:"scenario,omitempty"`
YourMission string `json:"your_mission"` YourMission string `json:"your_mission"`
Rules string `json:"rules"` Rules string `json:"rules"`
Start time.Time `json:"start"` Start time.Time `json:"start"`
@ -28,7 +28,7 @@ type SessionZQDS struct {
Challenges []*ChallengeZQDS `json:"challenges"` Challenges []*ChallengeZQDS `json:"challenges"`
} }
func GenZQDSSessionFile(s *settings.Settings) (*SessionZQDS, error) { func GenZQDSSessionFile(c *settings.ChallengeInfo, s *settings.Settings) (*SessionZQDS, error) {
themes, err := GetThemes() themes, err := GetThemes()
if err != nil { if err != nil {
return nil, err return nil, err
@ -58,11 +58,10 @@ func GenZQDSSessionFile(s *settings.Settings) (*SessionZQDS, error) {
} }
return &SessionZQDS{ return &SessionZQDS{
Name: "Challenge Forensic", Name: c.Title,
Description: "", Description: c.Description,
Scenario: "", Rules: c.Rules,
YourMission: "", YourMission: c.YourMission,
Rules: "",
Start: s.Start, Start: s.Start,
End: s.End, End: s.End,
Challenges: challenges, Challenges: challenges,

59
settings/challenge.go Normal file
View File

@ -0,0 +1,59 @@
package settings
import (
"encoding/json"
"os"
)
// ChallengeFile is the expected name of the file containing the challenge infos.
const ChallengeFile = "challenge.json"
// ChallengeInfo stores common descriptions and informations about the challenge.
type ChallengeInfo struct {
// Title is the displayed name of the challenge.
Title string `json:"title"`
// Authors is the group name of people making the challenge.
Authors string `json:"authors"`
// VideoLink is the link to explaination videos when the challenge is over.
VideosLink string `json:"videoslink"`
// Description gives an overview of the challenge.
Description string `json:"description"`
// Rules tell the player some help.
Rules string `json:"rules"`
// YourMission is a small introduction to understand the goals.
YourMission string `json:"your_mission"`
}
// ReadChallenge parses the file at the given location.
func ReadChallengeInfo(path string) (*ChallengeInfo, error) {
var s ChallengeInfo
if fd, err := os.Open(path); err != nil {
return nil, err
} else {
defer fd.Close()
jdec := json.NewDecoder(fd)
if err := jdec.Decode(&s); err != nil {
return &s, err
}
return &s, nil
}
}
// SaveChallenge saves challenge at the given location.
func SaveChallengeInfo(path string, s *ChallengeInfo) error {
if fd, err := os.Create(path); err != nil {
return err
} else {
defer fd.Close()
jenc := json.NewEncoder(fd)
if err := jenc.Encode(s); err != nil {
return err
}
return nil
}
}

View File

@ -22,12 +22,6 @@ var SettingsDir string = "./SETTINGS"
// Settings represents the settings panel. // Settings represents the settings panel.
type Settings struct { type Settings struct {
// Title is the displayed name of the challenge.
Title string `json:"title"`
// Authors is the group name of people making the challenge.
Authors string `json:"authors"`
// VideoLink is the link to explaination videos when the challenge is over.
VideosLink string `json:"videoslink"`
// WorkInProgress indicates if the current challenge is under development or if it is in production. // WorkInProgress indicates if the current challenge is under development or if it is in production.
WorkInProgress bool `json:"wip,omitempty"` WorkInProgress bool `json:"wip,omitempty"`