server/libfic/flag_key.go

403 lines
13 KiB
Go

package fic
import (
"bytes"
"errors"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"time"
"golang.org/x/crypto/blake2b"
)
// FlagKey represents a flag's challenge, stored as hash.
type FlagKey struct {
Id int `json:"id"`
// IdExercice is the identifier of the underlying challenge
IdExercice int64 `json:"idExercice"`
// Order is used to sort the flag between them
Order int8 `json:"order"`
// Label is the title of the flag as displayed to players
Label string `json:"label"`
// Type is the kind of flag
Type string `json:"type,omitempty"`
// Placeholder is a small piece of text that aims to add useful information like flag format, ...
Placeholder string `json:"placeholder"`
// Help is a description of the flag
Help string `json:"help"`
// Unit is another indication appended to the input
Unit string `json:"unit"`
// IgnoreCase indicates if the case is sensitive to case or not
IgnoreCase bool `json:"ignorecase"`
// NoTrim indicates if the flag should not be trimed to avoid mistakes
NoTrim bool `json:"notrim,omitempty"`
// Multiline indicates if the flag is stored on multiple lines
Multiline bool `json:"multiline"`
// CaptureRegexp extracts a subset of the player's answer, that will be checked.
CaptureRegexp *string `json:"capture_regexp"`
// SortReGroups indicates if groups resulting of the validator regexp should be sorted.
SortReGroups bool `json:"sort_re_grps"`
// Checksum is the expected hashed flag
Checksum []byte `json:"value"`
// ChoicesCost is the number of points lost to display choices.
ChoicesCost int32 `json:"choices_cost"`
// BonusGain makes the flag completion optionnal. If it is filled and correct, it gives some points.
BonusGain int32 `json:"bonus_gain"`
}
func AnalyzeNumberFlag(t string) (min, max, step *float64, err error) {
fields := strings.Split(t, ",")
if len(fields) != 4 || fields[0] != "number" {
err = errors.New("this is not a flag of type 'number'")
return
}
if len(fields[1]) > 0 {
min = new(float64)
*min, err = strconv.ParseFloat(fields[1], 64)
if err != nil {
return
}
}
if len(fields[2]) > 0 {
max = new(float64)
*max, err = strconv.ParseFloat(fields[2], 64)
if err != nil {
return
}
}
step = new(float64)
if len(fields[3]) > 0 {
*step, err = strconv.ParseFloat(fields[3], 64)
if err != nil {
return
}
} else {
*step = 1
}
return
}
func (k *FlagKey) AnalyzeFlagLabel() (label, separator string, ignoreorder bool, nblines uint64) {
if k.Label[0] == '`' && len(k.Label) > 4 {
label = k.Label[4:]
separator = string(k.Label[1])
ignoreorder = k.Label[2] == 't'
nblines, _ = strconv.ParseUint(string(k.Label[3]), 10, 8)
} else {
label = k.Label
}
return
}
// GetFlagKeys returns a list of key's flags comming with the challenge.
func (e *Exercice) GetFlagKeys() ([]*FlagKey, error) {
if rows, err := DBQuery("SELECT id_flag, id_exercice, ordre, label, type, placeholder, help, unit, ignorecase, notrim, multiline, validator_regexp, sort_re_grps, cksum, choices_cost, bonus_gain FROM exercice_flags WHERE id_exercice = ?", e.Id); err != nil {
return nil, err
} else {
defer rows.Close()
flags := []*FlagKey{}
for rows.Next() {
k := &FlagKey{}
k.IdExercice = e.Id
if err := rows.Scan(&k.Id, &k.IdExercice, &k.Order, &k.Label, &k.Type, &k.Placeholder, &k.Help, &k.Unit, &k.IgnoreCase, &k.NoTrim, &k.Multiline, &k.CaptureRegexp, &k.SortReGroups, &k.Checksum, &k.ChoicesCost, &k.BonusGain); err != nil {
return nil, err
}
flags = append(flags, k)
}
if err := rows.Err(); err != nil {
return nil, err
}
return flags, nil
}
}
// GetFlagKey returns a list of flags comming with the challenge.
func GetFlagKey(id int) (k *FlagKey, err error) {
k = &FlagKey{}
err = DBQueryRow("SELECT id_flag, id_exercice, ordre, label, type, placeholder, help, unit, ignorecase, notrim, multiline, validator_regexp, sort_re_grps, cksum, choices_cost, bonus_gain FROM exercice_flags WHERE id_flag = ?", id).Scan(&k.Id, &k.IdExercice, &k.Order, &k.Label, &k.Type, &k.Placeholder, &k.Help, &k.Unit, &k.IgnoreCase, &k.Multiline, &k.NoTrim, &k.CaptureRegexp, &k.SortReGroups, &k.Checksum, &k.ChoicesCost, &k.BonusGain)
return
}
// GetFlagKey returns a flag.
func (e *Exercice) GetFlagKey(id int) (k *FlagKey, err error) {
k = &FlagKey{}
err = DBQueryRow("SELECT id_flag, id_exercice, ordre, label, type, placeholder, help, unit, ignorecase, notrim, multiline, validator_regexp, sort_re_grps, cksum, choices_cost, bonus_gain FROM exercice_flags WHERE id_flag = ? AND id_exercice = ?", id, e.Id).Scan(&k.Id, &k.IdExercice, &k.Order, &k.Label, &k.Type, &k.Placeholder, &k.Help, &k.Unit, &k.IgnoreCase, &k.NoTrim, &k.Multiline, &k.CaptureRegexp, &k.SortReGroups, &k.Checksum, &k.ChoicesCost, &k.BonusGain)
return
}
// GetFlagKeyByLabel returns a flag matching the given label.
func (e *Exercice) GetFlagKeyByLabel(label string) (k *FlagKey, err error) {
k = &FlagKey{}
err = DBQueryRow("SELECT id_flag, id_exercice, ordre, label, type, placeholder, help, unit, ignorecase, notrim, multiline, validator_regexp, sort_re_grps, cksum, choices_cost, bonus_gain FROM exercice_flags WHERE label LIKE ? AND id_exercice = ?", label, e.Id).Scan(&k.Id, &k.IdExercice, &k.Order, &k.Label, &k.Type, &k.Placeholder, &k.Help, &k.Unit, &k.IgnoreCase, &k.NoTrim, &k.Multiline, &k.CaptureRegexp, &k.SortReGroups, &k.Checksum, &k.ChoicesCost, &k.BonusGain)
return
}
// ComputeHashedFlag calculates the expected checksum for the given raw_value.
func ComputeHashedFlag(raw_value []byte, ignorecase bool, notrim bool, capture_regexp *string, sortregroups bool) (hash [blake2b.Size]byte, err error) {
if ignorecase {
raw_value = bytes.ToLower(raw_value)
}
if !notrim {
raw_value = bytes.TrimSpace(raw_value)
}
// Check that raw value passes through the regexp
if capture_regexp != nil {
if raw_value, err = ExecCaptureRegexp(*capture_regexp, raw_value, ignorecase, sortregroups); err != nil {
return
}
}
// Check that the value is not empty
if len(raw_value) == 0 {
err = errors.New("empty flag after applying filters")
}
hash = blake2b.Sum512(raw_value)
return
}
func ExecCaptureRegexp(vre string, val []byte, ignorecase bool, sortregroups bool) ([]byte, error) {
if ignorecase {
vre = "(?i)" + vre
}
if re, err := regexp.Compile(vre); err != nil {
return val, err
} else if res := re.FindSubmatch(val); res == nil {
return val, errors.New("expected flag doesn't pass through the capture_regexp")
} else if sortregroups && len(res) > 2 {
var tab []string
for _, v := range res[1:] {
tab = append(tab, string(v))
}
sort.Strings(tab)
return []byte(strings.Join(tab, "+")), nil
} else {
return bytes.Join(res[1:], []byte("+")), nil
}
}
// AddRawFlagKey creates and fills a new struct FlagKey, from a non-hashed flag, and registers it into the database.
func (e *Exercice) AddRawFlagKey(name string, t string, placeholder string, ignorecase bool, multiline bool, notrim bool, capture_regexp *string, sortregroups bool, raw_value []byte, choicescost int32, bonusgain int32) (*FlagKey, error) {
hash, err := ComputeHashedFlag(raw_value, ignorecase, notrim, capture_regexp, sortregroups)
if err != nil {
return nil, err
}
f := &FlagKey{
Type: t,
Label: name,
Placeholder: placeholder,
IgnoreCase: ignorecase,
NoTrim: notrim,
Multiline: multiline,
CaptureRegexp: capture_regexp,
SortReGroups: sortregroups,
Checksum: hash[:],
ChoicesCost: choicescost,
BonusGain: bonusgain,
}
_, err = f.Create(e)
return f, err
}
// GetId returns the Flag identifier.
func (k *FlagKey) GetId() int {
return k.Id
}
// RecoverId returns the Flag identifier as register in DB.
func (k *FlagKey) RecoverId() (Flag, error) {
if err := DBQueryRow("SELECT id_flag FROM exercice_flags WHERE label LIKE ? AND id_exercice = ?", k.Label, k.IdExercice).Scan(&k.Id); err != nil {
return nil, err
} else {
return k, err
}
}
// AddFlagKey creates and fills a new struct Flag, from a hashed flag, and registers it into the database.
func (k *FlagKey) Create(e *Exercice) (Flag, error) {
// Check the regexp compile
if k.CaptureRegexp != nil {
if _, err := regexp.Compile(*k.CaptureRegexp); err != nil {
return k, err
}
}
if res, err := DBExec("INSERT INTO exercice_flags (id_exercice, ordre, label, type, placeholder, help, unit, ignorecase, notrim, multiline, validator_regexp, sort_re_grps, cksum, choices_cost, bonus_gain) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", e.Id, k.Order, k.Label, k.Type, k.Placeholder, k.Help, k.Unit, k.IgnoreCase, k.NoTrim, k.Multiline, k.CaptureRegexp, k.SortReGroups, k.Checksum, k.ChoicesCost, k.BonusGain); err != nil {
return k, err
} else if kid, err := res.LastInsertId(); err != nil {
return k, err
} else {
k.Id = int(kid)
k.IdExercice = e.Id
return k, nil
}
}
// ComputeChecksum calculates the checksum for a given value.
func (k *FlagKey) ComputeChecksum(val []byte) ([]byte, error) {
cksum, err := ComputeHashedFlag(val, k.IgnoreCase, k.NoTrim, k.CaptureRegexp, k.SortReGroups)
return cksum[:], err
}
// Update applies modifications back to the database.
func (k *FlagKey) Update() (int64, error) {
if k.CaptureRegexp != nil {
if _, err := regexp.Compile(*k.CaptureRegexp); err != nil {
return 0, err
}
}
if res, err := DBExec("UPDATE exercice_flags SET id_exercice = ?, ordre = ?, label = ?, type = ?, placeholder = ?, help = ?, unit = ?, ignorecase = ?, notrim = ?, multiline = ?, validator_regexp = ?, sort_re_grps = ?, cksum = ?, choices_cost = ?, bonus_gain = ? WHERE id_flag = ?", k.IdExercice, k.Order, k.Label, k.Type, k.Placeholder, k.Help, k.Unit, k.IgnoreCase, k.NoTrim, k.Multiline, k.CaptureRegexp, k.SortReGroups, k.Checksum, k.ChoicesCost, k.BonusGain, k.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}
// Delete the flag from the database.
func (k *FlagKey) Delete() (int64, error) {
if _, err := DBExec("DELETE FROM exercice_files_okey_deps WHERE id_flag = ?", k.Id); err != nil {
return 0, err
} else if _, err := DBExec("DELETE FROM exercice_mcq_okey_deps WHERE id_flag_dep = ?", k.Id); err != nil {
return 0, err
} else if _, err := DBExec("DELETE FROM exercice_flags_omcq_deps WHERE id_flag = ?", k.Id); err != nil {
return 0, err
} else if _, err := DBExec("DELETE FROM exercice_flags_deps WHERE id_flag = ? OR id_flag_dep = ?", k.Id, k.Id); err != nil {
return 0, err
} else if _, err := DBExec("DELETE FROM exercice_hints_okey_deps WHERE id_flag_dep = ?", k.Id); err != nil {
return 0, err
} else if _, err := DBExec("DELETE FROM flag_choices WHERE id_flag = ?", k.Id); err != nil {
return 0, err
} else if res, err := DBExec("DELETE FROM exercice_flags WHERE id_flag = ?", k.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}
func (k *FlagKey) GetOrder() int8 {
return k.Order
}
// AddDepend insert a new dependency to a given flag.
func (k *FlagKey) AddDepend(j Flag) (err error) {
if d, ok := j.(*FlagKey); ok {
_, err = DBExec("INSERT INTO exercice_flags_deps (id_flag, id_flag_dep) VALUES (?, ?)", k.Id, d.Id)
} else if d, ok := j.(*MCQ); ok {
_, err = DBExec("INSERT INTO exercice_flags_omcq_deps (id_flag, id_mcq_dep) VALUES (?, ?)", k.Id, d.Id)
} else {
err = fmt.Errorf("dependancy type for key (%T) not implemented for this flag", j)
}
return
}
// GetDepends retrieve the flag's dependency list.
func (k *FlagKey) GetDepends() ([]Flag, error) {
var deps []Flag
if rows, err := DBQuery("SELECT id_flag_dep FROM exercice_flags_deps WHERE id_flag = ?", k.Id); err != nil {
return nil, err
} else {
defer rows.Close()
for rows.Next() {
var d int
if err := rows.Scan(&d); err != nil {
return nil, err
}
deps = append(deps, &FlagKey{Id: d, IdExercice: k.IdExercice})
}
if err := rows.Err(); err != nil {
return nil, err
}
}
if rows, err := DBQuery("SELECT id_mcq_dep FROM exercice_flags_omcq_deps WHERE id_flag = ?", k.Id); err != nil {
return nil, err
} else {
defer rows.Close()
for rows.Next() {
var d int
if err := rows.Scan(&d); err != nil {
return nil, err
}
deps = append(deps, &MCQ{Id: d, IdExercice: k.IdExercice})
}
if err := rows.Err(); err != nil {
return nil, err
}
}
return deps, nil
}
// IsOptionnal to know if the flag can be omitted when validating the step.
func (k *FlagKey) IsOptionnal() bool {
return k.BonusGain != 0
}
// Check if the given val is the expected one for this flag.
func (k *FlagKey) Check(v interface{}) int {
var val []byte
if va, ok := v.([]byte); !ok {
return -1
} else {
val = va
}
hash, err := k.ComputeChecksum(val)
if err != nil {
return 1
}
if len(k.Checksum) != len(hash) {
return 1
}
for i := range hash {
if k.Checksum[i] != hash[i] {
return 1
}
}
return 0
}
// FoundBy registers in the database that the given Team solved the flag.
func (k *FlagKey) FoundBy(t *Team) (err error) {
_, err = DBExec("INSERT INTO flag_found (id_flag, id_team, time) VALUES (?, ?, ?)", k.Id, t.Id, time.Now())
return
}
// GetExercice returns the parent Exercice where this flag can be found.
func (k *FlagKey) GetExercice() (*Exercice, error) {
var eid int64
if err := DBQueryRow("SELECT id_exercice FROM exercice_flags WHERE id_flag = ?", k.Id).Scan(&eid); err != nil {
return nil, err
}
return GetExercice(eid)
}