mirror of https://github.com/dirtbags/moth.git
Throttle submissions per team (more...)
* Preload team hashes in maintenance goroutine * Check team hash weirdness at maintenance time * Check team hash validity before checking answer * Rate limit answer submissions (adjustable with -attempt flag) * ... and more!
This commit is contained in:
parent
a36dbb48be
commit
1614b02b66
|
@ -0,0 +1 @@
|
|||
* Figure out how to log JSend short text in addition to HTTP code
|
|
@ -0,0 +1,18 @@
|
|||
Summary: Answer patterns
|
||||
Answer: command.com
|
||||
Answer: COMMAND.COM
|
||||
Author: neale
|
||||
Pattern: [0-9A-Za-z]{1,8}\.[A-Za-z]{1,3}
|
||||
|
||||
This puzzle features answer input pattern checking.
|
||||
|
||||
Sometimes you need to provide a hint about whether the user has entered the answer in the right format.
|
||||
By providing a `Pattern` value (a regular expression),
|
||||
the browser will (hopefully) provide a visual hint when an answer is incorrectly formatted.
|
||||
It will also (hopefully) prevent the user from submitting,
|
||||
which will (hopefully) inform the participant that they may have the right solution technique,
|
||||
but there's a problem with the format of the answer.
|
||||
This will (hopefully) keep people from getting overly-frustrated with difficult-to-enter answers.
|
||||
|
||||
This answer field will validate only FAT 8+3 filenames.
|
||||
Try it!
|
101
src/handlers.go
101
src/handlers.go
|
@ -57,36 +57,10 @@ func hasLine(r io.Reader, line string) bool {
|
|||
}
|
||||
|
||||
func (ctx *Instance) registerHandler(w http.ResponseWriter, req *http.Request) {
|
||||
teamname := req.FormValue("name")
|
||||
teamid := req.FormValue("id")
|
||||
teamName := req.FormValue("name")
|
||||
teamId := req.FormValue("id")
|
||||
|
||||
// Keep foolish operators from shooting themselves in the foot
|
||||
// You would have to add a pathname to your list of Team IDs to open this vulnerability,
|
||||
// but I have learned not to overestimate people.
|
||||
if strings.Contains(teamid, "../") {
|
||||
teamid = "rodney"
|
||||
}
|
||||
|
||||
if (teamid == "") || (teamname == "") {
|
||||
respond(
|
||||
w, req, JSendFail,
|
||||
"Invalid Entry",
|
||||
"Either `id` or `name` was missing from this request.",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
teamids, err := os.Open(ctx.StatePath("teamids.txt"))
|
||||
if err != nil {
|
||||
respond(
|
||||
w, req, JSendFail,
|
||||
"Cannot read valid team IDs",
|
||||
"An error was encountered trying to read valid teams IDs: %v", err,
|
||||
)
|
||||
return
|
||||
}
|
||||
defer teamids.Close()
|
||||
if !hasLine(teamids, teamid) {
|
||||
if !ctx.ValidTeamId(teamId) {
|
||||
respond(
|
||||
w, req, JSendFail,
|
||||
"Invalid Team ID",
|
||||
|
@ -95,38 +69,57 @@ func (ctx *Instance) registerHandler(w http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(ctx.StatePath("teams", teamid), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
|
||||
if os.IsExist(err) {
|
||||
respond(
|
||||
w, req, JSendFail,
|
||||
"Already registered",
|
||||
"This team ID has already been registered.",
|
||||
)
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Print(err)
|
||||
respond(
|
||||
w, req, JSendFail,
|
||||
"Registration failed",
|
||||
"Unable to register. Perhaps a teammate has already registered?",
|
||||
)
|
||||
f, err := os.OpenFile(ctx.StatePath("teams", teamId), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
if os.IsExist(err) {
|
||||
respond(
|
||||
w, req, JSendFail,
|
||||
"Already registered",
|
||||
"This team ID has already been registered.",
|
||||
)
|
||||
} else {
|
||||
log.Print(err)
|
||||
respond(
|
||||
w, req, JSendFail,
|
||||
"Registration failed",
|
||||
"Unable to register. Perhaps a teammate has already registered?",
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
fmt.Fprintln(f, teamname)
|
||||
|
||||
fmt.Fprintln(f, teamName)
|
||||
respond(
|
||||
w, req, JSendSuccess,
|
||||
"Team registered",
|
||||
"Okay, your team has been named and you may begin using your team ID!",
|
||||
"Your team has been named and you may begin using your team ID!",
|
||||
)
|
||||
}
|
||||
|
||||
func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) {
|
||||
teamid := req.FormValue("id")
|
||||
teamId := req.FormValue("id")
|
||||
category := req.FormValue("cat")
|
||||
pointstr := req.FormValue("points")
|
||||
answer := req.FormValue("answer")
|
||||
|
||||
if ! ctx.ValidTeamId(teamId) {
|
||||
respond(
|
||||
w, req, JSendFail,
|
||||
"Invalid team ID",
|
||||
"That team ID is not valid for this event.",
|
||||
)
|
||||
return
|
||||
}
|
||||
if ctx.TooFast(teamId) {
|
||||
respond(
|
||||
w, req, JSendFail,
|
||||
"Submitting too quickly",
|
||||
"Your team can only submit one answer every %v", ctx.AttemptInterval,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
points, err := strconv.Atoi(pointstr)
|
||||
if err != nil {
|
||||
respond(
|
||||
|
@ -159,7 +152,7 @@ func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := ctx.AwardPoints(teamid, category, points); err != nil {
|
||||
if err := ctx.AwardPoints(teamId, category, points); err != nil {
|
||||
respond(
|
||||
w, req, JSendError,
|
||||
"Cannot award points",
|
||||
|
@ -170,14 +163,14 @@ func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) {
|
|||
respond(
|
||||
w, req, JSendSuccess,
|
||||
"Points awarded",
|
||||
fmt.Sprintf("%d points for %s!", points, teamid),
|
||||
fmt.Sprintf("%d points for %s!", points, teamId),
|
||||
)
|
||||
}
|
||||
|
||||
func (ctx *Instance) puzzlesHandler(w http.ResponseWriter, req *http.Request) {
|
||||
teamid := req.FormValue("id")
|
||||
if _, err := ctx.TeamName(teamid); err != nil {
|
||||
http.Error(w, "Unauthorized: must provide team ID", http.StatusUnauthorized)
|
||||
teamId := req.FormValue("id")
|
||||
if _, err := ctx.TeamName(teamId); err != nil {
|
||||
http.Error(w, "Must provide team ID", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -210,7 +203,7 @@ func (ctx *Instance) contentHandler(w http.ResponseWriter, req *http.Request) {
|
|||
puzzleId := parts[len(parts)-2]
|
||||
categoryName := parts[len(parts)-3]
|
||||
|
||||
mb, ok := ctx.Categories[categoryName]
|
||||
mb, ok := ctx.categories[categoryName]
|
||||
if !ok {
|
||||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
return
|
||||
|
@ -238,7 +231,7 @@ func (ctx *Instance) staticHandler(w http.ResponseWriter, req *http.Request) {
|
|||
path = "/index.html"
|
||||
}
|
||||
|
||||
f, err := os.Open(ctx.ResourcePath(path))
|
||||
f, err := os.Open(ctx.ThemePath(path))
|
||||
if err != nil {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
|
|
|
@ -15,40 +15,39 @@ import (
|
|||
)
|
||||
|
||||
type Instance struct {
|
||||
Base string
|
||||
MothballDir string
|
||||
StateDir string
|
||||
ResourcesDir string
|
||||
Categories map[string]*Mothball
|
||||
update chan bool
|
||||
jPuzzleList []byte
|
||||
jPointsLog []byte
|
||||
mux *http.ServeMux
|
||||
Base string
|
||||
MothballDir string
|
||||
StateDir string
|
||||
ThemeDir string
|
||||
AttemptInterval time.Duration
|
||||
|
||||
categories map[string]*Mothball
|
||||
update chan bool
|
||||
jPuzzleList []byte
|
||||
jPointsLog []byte
|
||||
nextAttempt map[string]time.Time
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
func NewInstance(base, mothballDir, stateDir, resourcesDir string) (*Instance, error) {
|
||||
ctx := &Instance{
|
||||
Base: strings.TrimRight(base, "/"),
|
||||
MothballDir: mothballDir,
|
||||
StateDir: stateDir,
|
||||
ResourcesDir: resourcesDir,
|
||||
Categories: map[string]*Mothball{},
|
||||
update: make(chan bool, 10),
|
||||
mux: http.NewServeMux(),
|
||||
func (ctx *Instance) Initialize() error {
|
||||
// Roll over and die if directories aren't even set up
|
||||
if _, err := os.Stat(ctx.MothballDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(ctx.StateDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Roll over and die if directories aren't even set up
|
||||
if _, err := os.Stat(mothballDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := os.Stat(stateDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx.Base = strings.TrimRight(ctx.Base, "/")
|
||||
ctx.categories = map[string]*Mothball{}
|
||||
ctx.update = make(chan bool, 10)
|
||||
ctx.nextAttempt = map[string]time.Time{}
|
||||
ctx.mux = http.NewServeMux()
|
||||
|
||||
ctx.BindHandlers()
|
||||
ctx.MaybeInitialize()
|
||||
|
||||
return ctx, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stuff people with mediocre handwriting could write down unambiguously, and can be entered without holding down shift
|
||||
|
@ -123,9 +122,16 @@ func (ctx *Instance) StatePath(parts ...string) string {
|
|||
return path.Join(ctx.StateDir, tail)
|
||||
}
|
||||
|
||||
func (ctx *Instance) ResourcePath(parts ...string) string {
|
||||
func (ctx *Instance) ThemePath(parts ...string) string {
|
||||
tail := pathCleanse(parts)
|
||||
return path.Join(ctx.ResourcesDir, tail)
|
||||
return path.Join(ctx.ThemeDir, tail)
|
||||
}
|
||||
|
||||
func (ctx *Instance) TooFast(teamId string) bool {
|
||||
now := time.Now()
|
||||
next, _ := ctx.nextAttempt[teamId]
|
||||
ctx.nextAttempt[teamId] = now.Add(ctx.AttemptInterval)
|
||||
return now.Before(next)
|
||||
}
|
||||
|
||||
func (ctx *Instance) PointsLog() []*Award {
|
||||
|
@ -153,20 +159,20 @@ func (ctx *Instance) PointsLog() []*Award {
|
|||
return ret
|
||||
}
|
||||
|
||||
// awardPoints gives points to teamid in category.
|
||||
// AwardPoints gives points to teamId in category.
|
||||
// It first checks to make sure these are not duplicate points.
|
||||
// This is not a perfect check, you can trigger a race condition here.
|
||||
// It's just a courtesy to the user.
|
||||
// The maintenance task makes sure we never have duplicate points in the log.
|
||||
func (ctx *Instance) AwardPoints(teamid, category string, points int) error {
|
||||
func (ctx *Instance) AwardPoints(teamId, category string, points int) error {
|
||||
a := Award{
|
||||
When: time.Now(),
|
||||
TeamId: teamid,
|
||||
TeamId: teamId,
|
||||
Category: category,
|
||||
Points: points,
|
||||
}
|
||||
|
||||
teamName, err := ctx.TeamName(teamid)
|
||||
_, err := ctx.TeamName(teamId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("No registered team with this hash")
|
||||
}
|
||||
|
@ -177,7 +183,7 @@ func (ctx *Instance) AwardPoints(teamid, category string, points int) error {
|
|||
}
|
||||
}
|
||||
|
||||
fn := fmt.Sprintf("%s-%s-%d", teamid, category, points)
|
||||
fn := fmt.Sprintf("%s-%s-%d", teamId, category, points)
|
||||
tmpfn := ctx.StatePath("points.tmp", fn)
|
||||
newfn := ctx.StatePath("points.new", fn)
|
||||
|
||||
|
@ -190,12 +196,12 @@ func (ctx *Instance) AwardPoints(teamid, category string, points int) error {
|
|||
}
|
||||
|
||||
ctx.update <- true
|
||||
log.Printf("Award %s %s %d", teamName, category, points)
|
||||
log.Printf("Award %s %s %d", teamId, category, points)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ctx *Instance) OpenCategoryFile(category string, parts ...string) (io.ReadCloser, error) {
|
||||
mb, ok := ctx.Categories[category]
|
||||
mb, ok := ctx.categories[category]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("No such category: %s", category)
|
||||
}
|
||||
|
@ -205,6 +211,11 @@ func (ctx *Instance) OpenCategoryFile(category string, parts ...string) (io.Read
|
|||
return f, err
|
||||
}
|
||||
|
||||
func (ctx *Instance) ValidTeamId(teamId string) bool {
|
||||
_, ok := ctx.nextAttempt[teamId]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (ctx *Instance) TeamName(teamId string) (string, error) {
|
||||
teamNameBytes, err := ioutil.ReadFile(ctx.StatePath("teams", teamId))
|
||||
teamName := strings.TrimSpace(string(teamNameBytes))
|
||||
|
|
|
@ -40,7 +40,7 @@ func (ctx *Instance) generatePuzzleList() {
|
|||
}
|
||||
|
||||
ret := map[string][]PuzzleMap{}
|
||||
for catName, mb := range ctx.Categories {
|
||||
for catName, mb := range ctx.categories {
|
||||
mf, err := mb.Open("map.txt")
|
||||
if err != nil {
|
||||
// File isn't in there
|
||||
|
@ -125,12 +125,12 @@ func (ctx *Instance) tidy() {
|
|||
ctx.MaybeInitialize()
|
||||
|
||||
// Refresh all current categories
|
||||
for categoryName, mb := range ctx.Categories {
|
||||
for categoryName, mb := range ctx.categories {
|
||||
if err := mb.Refresh(); err != nil {
|
||||
// Backing file vanished: remove this category
|
||||
log.Printf("Removing category: %s: %s", categoryName, err)
|
||||
mb.Close()
|
||||
delete(ctx.Categories, categoryName)
|
||||
delete(ctx.categories, categoryName)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -147,18 +147,64 @@ func (ctx *Instance) tidy() {
|
|||
}
|
||||
categoryName := strings.TrimSuffix(filename, ".mb")
|
||||
|
||||
if _, ok := ctx.Categories[categoryName]; !ok {
|
||||
if _, ok := ctx.categories[categoryName]; !ok {
|
||||
mb, err := OpenMothball(filepath)
|
||||
if err != nil {
|
||||
log.Printf("Error opening %s: %s", filepath, err)
|
||||
continue
|
||||
}
|
||||
log.Printf("New category: %s", filename)
|
||||
ctx.Categories[categoryName] = mb
|
||||
ctx.categories[categoryName] = mb
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readTeams reads in the list of team IDs,
|
||||
// so we can quickly validate them.
|
||||
func (ctx *Instance) readTeams() {
|
||||
filepath := ctx.StatePath("teamids.txt")
|
||||
teamids, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
log.Printf("Error openining %s: %s", filepath, err)
|
||||
return
|
||||
}
|
||||
defer teamids.Close()
|
||||
|
||||
// List out team IDs
|
||||
newList := map[string]bool{}
|
||||
scanner := bufio.NewScanner(teamids)
|
||||
for scanner.Scan() {
|
||||
teamId := scanner.Text()
|
||||
if (teamId == "..") || strings.ContainsAny(teamId, "/") {
|
||||
log.Printf("Dangerous team ID dropped: %s", teamId)
|
||||
continue
|
||||
}
|
||||
newList[scanner.Text()] = true
|
||||
}
|
||||
|
||||
// For any new team IDs, set their next attempt time to right now
|
||||
now := time.Now()
|
||||
added := 0
|
||||
for k, _ := range newList {
|
||||
if _, ok := ctx.nextAttempt[k]; !ok {
|
||||
ctx.nextAttempt[k] = now
|
||||
added += 1
|
||||
}
|
||||
}
|
||||
|
||||
// For any removed team IDs, remove them
|
||||
removed := 0
|
||||
for k, _ := range ctx.nextAttempt {
|
||||
if _, ok := newList[k]; !ok {
|
||||
delete(ctx.nextAttempt, k)
|
||||
}
|
||||
}
|
||||
|
||||
if (added > 0) || (removed > 0) {
|
||||
log.Printf("Team IDs updated: %d added, %d removed", added, removed)
|
||||
}
|
||||
}
|
||||
|
||||
// collectPoints gathers up files in points.new/ and appends their contents to points.log,
|
||||
// removing each points.new/ file as it goes.
|
||||
func (ctx *Instance) collectPoints() {
|
||||
|
@ -236,6 +282,7 @@ func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) {
|
|||
for {
|
||||
if ctx.isEnabled() {
|
||||
ctx.tidy()
|
||||
ctx.readTeams()
|
||||
ctx.collectPoints()
|
||||
ctx.generatePuzzleList()
|
||||
ctx.generatePointsLog()
|
||||
|
|
24
src/mothd.go
24
src/mothd.go
|
@ -15,30 +15,42 @@ func setup() error {
|
|||
}
|
||||
|
||||
func main() {
|
||||
base := flag.String(
|
||||
ctx := &Instance{}
|
||||
|
||||
flag.StringVar(
|
||||
&ctx.Base,
|
||||
"base",
|
||||
"/",
|
||||
"Base URL of this instance",
|
||||
)
|
||||
mothballDir := flag.String(
|
||||
flag.StringVar(
|
||||
&ctx.MothballDir,
|
||||
"mothballs",
|
||||
"/mothballs",
|
||||
"Path to read mothballs",
|
||||
)
|
||||
stateDir := flag.String(
|
||||
flag.StringVar(
|
||||
&ctx.StateDir,
|
||||
"state",
|
||||
"/state",
|
||||
"Path to write state",
|
||||
)
|
||||
themeDir := flag.String(
|
||||
flag.StringVar(
|
||||
&ctx.ThemeDir,
|
||||
"theme",
|
||||
"/theme",
|
||||
"Path to static theme resources (HTML, images, css, ...)",
|
||||
)
|
||||
flag.DurationVar(
|
||||
&ctx.AttemptInterval,
|
||||
"attempt",
|
||||
500*time.Millisecond,
|
||||
"Per-team time required between answer attempts",
|
||||
)
|
||||
maintenanceInterval := flag.Duration(
|
||||
"maint",
|
||||
20*time.Second,
|
||||
"Maintenance interval",
|
||||
"Time between maintenance tasks",
|
||||
)
|
||||
listen := flag.String(
|
||||
"listen",
|
||||
|
@ -51,7 +63,7 @@ func main() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, err := NewInstance(*base, *mothballDir, *stateDir, *themeDir)
|
||||
err := ctx.Initialize()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue