393 lines
12 KiB
Go
393 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
const domain = "https://klice.h21.cz"
|
|
const dbFile = "./klice.db"
|
|
|
|
const (
|
|
smallHelpPenalty = 5
|
|
giveUpPenalty = 30
|
|
)
|
|
|
|
var db *sql.DB
|
|
|
|
func hashPassword(password string) string {
|
|
hash := sha256.Sum256([]byte(password))
|
|
return hex.EncodeToString(hash[:])
|
|
}
|
|
|
|
func loginHandler(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodPost:
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "Could not parse form", http.StatusBadRequest)
|
|
return
|
|
}
|
|
password := r.FormValue("password")
|
|
hashedPassword := hashPassword(password)
|
|
|
|
err := db.QueryRow("SELECT 1 FROM teams WHERE password = ?", hashedPassword).Scan(new(int))
|
|
switch {
|
|
case err == sql.ErrNoRows:
|
|
err = LoginTemplate.Execute(w, true)
|
|
if err != nil {
|
|
http.Error(w, "Could not render template", http.StatusInternalServerError)
|
|
}
|
|
return
|
|
case err != nil:
|
|
http.Error(w, "Could not retrieve team", http.StatusInternalServerError)
|
|
return
|
|
default:
|
|
sessionID := hashedPassword
|
|
cookie := &http.Cookie{
|
|
Name: "session_id",
|
|
Value: sessionID,
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
SameSite: http.SameSiteStrictMode,
|
|
Secure: true,
|
|
}
|
|
http.SetCookie(w, cookie)
|
|
|
|
redir, err := r.Cookie("url")
|
|
if err == nil {
|
|
redir.MaxAge = -1
|
|
http.SetCookie(w, redir)
|
|
http.Redirect(w, r, safeRedirectURL(redir.Value), http.StatusSeeOther)
|
|
} else {
|
|
http.Redirect(w, r, "/team", http.StatusSeeOther)
|
|
}
|
|
}
|
|
case http.MethodGet:
|
|
err := LoginTemplate.Execute(w, false)
|
|
if err != nil {
|
|
http.Error(w, "Could not render template", http.StatusInternalServerError)
|
|
}
|
|
default:
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func logoutHandler(w http.ResponseWriter, r *http.Request) {
|
|
cookie := &http.Cookie{
|
|
Name: "session_id",
|
|
Value: "",
|
|
Path: "/",
|
|
MaxAge: -1,
|
|
HttpOnly: true,
|
|
SameSite: http.SameSiteStrictMode,
|
|
}
|
|
http.SetCookie(w, cookie)
|
|
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
}
|
|
|
|
func isLoggedIn(w http.ResponseWriter, r *http.Request) (bool, int) {
|
|
var exist bool = true
|
|
var teamID int
|
|
cookie, err := r.Cookie("session_id")
|
|
if err != nil {
|
|
exist = false
|
|
} else {
|
|
err = db.QueryRow("SELECT id FROM teams WHERE password = ?", cookie.Value).Scan(&teamID)
|
|
if err == sql.ErrNoRows {
|
|
exist = false
|
|
} else if err != nil {
|
|
exist = false
|
|
}
|
|
}
|
|
|
|
if !exist {
|
|
redir := &http.Cookie{
|
|
Name: "url",
|
|
Value: r.URL.String(),
|
|
MaxAge: 300,
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
SameSite: http.SameSiteStrictMode,
|
|
Secure: true,
|
|
}
|
|
http.SetCookie(w, redir)
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
return false, 0
|
|
}
|
|
return true, teamID
|
|
}
|
|
|
|
func teamInfoHandler(w http.ResponseWriter, r *http.Request) {
|
|
if loggedIn, teamID := isLoggedIn(w, r); loggedIn {
|
|
var teamName string
|
|
var difficultyLevel string
|
|
var lastCipher int
|
|
var penalty int
|
|
|
|
err := db.QueryRow("SELECT name, level_name, last_cipher, penalty FROM teams JOIN difficulty_levels ON teams.difficulty_level = difficulty_levels.id WHERE teams.id = ?", teamID).Scan(&teamName, &difficultyLevel, &lastCipher, &penalty)
|
|
if err != nil {
|
|
http.Error(w, "Could not retrieve team info", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
TeamTemplateData := TeamTemplateS{
|
|
TeamName: teamName,
|
|
Difficulty: difficultyLevel,
|
|
LastCipher: lastCipher,
|
|
Penalties: penalty,
|
|
}
|
|
|
|
err = TeamTemplate.Execute(w, TeamTemplateData)
|
|
if err != nil {
|
|
http.Error(w, "Could not render template", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func qrHandler(w http.ResponseWriter, r *http.Request) {
|
|
uid := r.PathValue("qr")
|
|
if uid == "" {
|
|
http.Error(w, "Invalid QR code", http.StatusBadRequest)
|
|
return
|
|
}
|
|
var positionID int
|
|
err := db.QueryRow("SELECT position_id FROM qr_codes WHERE uid = ?", uid).Scan(&positionID)
|
|
if err == sql.ErrNoRows {
|
|
http.Error(w, "QR code not found", http.StatusNotFound)
|
|
return
|
|
} else if err != nil {
|
|
http.Error(w, "Could not retrieve position", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if loggedIn, teamID := isLoggedIn(w, r); loggedIn {
|
|
var assignment string
|
|
var cipherID int
|
|
var taskID int
|
|
var order int
|
|
var last_cipher int
|
|
var help int = 0
|
|
var penalty int = 0
|
|
|
|
// Find task for this position and team's difficulty level
|
|
err = db.QueryRow("SELECT id FROM TASKS WHERE position_id = ? AND difficulty_level = (SELECT difficulty_level FROM teams WHERE id = ?)", positionID, teamID).Scan(&taskID)
|
|
if err == sql.ErrNoRows {
|
|
http.Error(w, "No task found for this position and team", http.StatusNotFound)
|
|
return
|
|
} else if err != nil {
|
|
http.Error(w, "Could not retrieve task", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
// get task order
|
|
err = db.QueryRow("SELECT order_num FROM TASKS WHERE id = ?", taskID).Scan(&order)
|
|
if err != nil {
|
|
http.Error(w, "Could not retrieve task order", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
// get last cipher visited by team
|
|
err = db.QueryRow("SELECT last_cipher FROM teams WHERE id = ?", teamID).Scan(&last_cipher)
|
|
if err != nil {
|
|
http.Error(w, "Could not retrieve last cipher", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
// check if the task is available for the team
|
|
// if order > last_cipher, task is not yet available
|
|
// if order == last_cipher, task is now available
|
|
// if order <= last_cipher, task has been already visited, allow viewing
|
|
if order > last_cipher {
|
|
http.Error(w, "This task is not yet available", http.StatusForbidden)
|
|
return
|
|
} else if order == last_cipher {
|
|
_, err := db.Exec("UPDATE teams SET last_loaded_cipher = ? WHERE id = ?", order, teamID)
|
|
if err != nil {
|
|
http.Error(w, "Could not update last loaded cipher", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
} else if order < last_cipher {
|
|
help = 2
|
|
}
|
|
// get cipher assignment
|
|
err = db.QueryRow("SELECT id, assignment FROM CIPHERS WHERE id = (SELECT cipher_id FROM TASKS WHERE id = ?)", taskID).Scan(&cipherID, &assignment)
|
|
if err == sql.ErrNoRows {
|
|
http.Error(w, "No cipher found", http.StatusNotFound)
|
|
return
|
|
} else if err != nil {
|
|
http.Error(w, "Could not retrieve cipher", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
CipherTemplateData := CipherTemplateS{
|
|
Order: uint(order),
|
|
Assignment: template.HTML(assignment),
|
|
HelpText: "",
|
|
FinalClue: "",
|
|
Coordinates: "",
|
|
Solution: "",
|
|
Wrong: false,
|
|
}
|
|
|
|
// get penalties for this task and team
|
|
if help == 0 {
|
|
err = db.QueryRow("SELECT minutes FROM penalties WHERE team_id = ? AND task_id = ?", teamID, taskID).Scan(&penalty)
|
|
if err == sql.ErrNoRows {
|
|
penalty = 0
|
|
} else if err != nil {
|
|
http.Error(w, "Could not retrieve penalties", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
// determine help level based on penalties
|
|
if penalty > 0 && penalty <= smallHelpPenalty {
|
|
help = 1
|
|
} else if penalty > smallHelpPenalty {
|
|
help = 2
|
|
}
|
|
}
|
|
|
|
// handle answer and help form submission
|
|
if r.Method == http.MethodPost {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "Could not parse form", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if r.FormValue("help") == "1" && help == 0 { // small help
|
|
help = 1
|
|
db.Exec("INSERT INTO penalties (team_id, task_id, minutes) VALUES (?, ?, ?)", teamID, taskID, smallHelpPenalty)
|
|
db.Exec("UPDATE teams SET penalty = penalty + ? WHERE id = ?", smallHelpPenalty, teamID)
|
|
} else if r.FormValue("help") == "2" && help == 1 { // give up
|
|
help = 2
|
|
db.Exec("UPDATE penalties SET minutes = minutes + ? WHERE team_id = ? AND task_id = ?", giveUpPenalty, teamID, taskID)
|
|
db.Exec("UPDATE teams SET penalty = penalty + ?, last_cipher = ? WHERE id = ?", giveUpPenalty, order+1, teamID)
|
|
} else if answer := r.FormValue("solution"); answer != "" && help < 2 { // answer submission
|
|
var correctAnswer string
|
|
err = db.QueryRow("SELECT solution FROM CIPHERS WHERE id = ?", cipherID).Scan(&correctAnswer)
|
|
if err != nil {
|
|
http.Error(w, "Could not retrieve solution", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if strings.EqualFold(strings.TrimSpace(answer), strings.TrimSpace(correctAnswer)) {
|
|
// correct answer, move to next task
|
|
db.Exec("UPDATE teams SET last_cipher = ? WHERE id = ?", order+1, teamID)
|
|
help = 2
|
|
} else {
|
|
CipherTemplateData.Wrong = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// find which clues to show
|
|
switch help {
|
|
case 1: // small help
|
|
var helpText string
|
|
err = db.QueryRow("SELECT clue FROM CIPHERS WHERE id = ?", cipherID).Scan(&helpText)
|
|
if err == sql.ErrNoRows {
|
|
helpText = ""
|
|
} else if err != nil {
|
|
http.Error(w, "Could not retrieve help text", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
CipherTemplateData.HelpText = template.HTML(helpText)
|
|
case 2: // next cipher
|
|
// get end clue
|
|
var endClue string
|
|
err = db.QueryRow("SELECT end_clue FROM TASKS WHERE id = ?", taskID).Scan(&endClue)
|
|
if err == sql.ErrNoRows {
|
|
endClue = ""
|
|
} else if err != nil {
|
|
http.Error(w, "Could not retrieve end clue", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
CipherTemplateData.FinalClue = endClue
|
|
// get coordinates
|
|
var coordinates, positionHint string
|
|
err = db.QueryRow("SELECT gps, clue FROM POSITIONS WHERE id = (SELECT position_id FROM TASKS WHERE id = (SELECT id FROM TASKS WHERE order_num = ? AND difficulty_level = (SELECT difficulty_level FROM teams WHERE id = ?)))", order+1, teamID).Scan(&coordinates, &positionHint)
|
|
if err == sql.ErrNoRows {
|
|
coordinates = "Konec, vraťte se."
|
|
positionHint = "KONEC"
|
|
} else if err != nil {
|
|
http.Error(w, "Could not retrieve coordinates", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
CipherTemplateData.Coordinates = coordinates
|
|
CipherTemplateData.PositionHint = positionHint
|
|
// get solution
|
|
var solution string
|
|
err = db.QueryRow("SELECT solution FROM CIPHERS WHERE id = ?", cipherID).Scan(&solution)
|
|
if err == sql.ErrNoRows {
|
|
solution = ""
|
|
} else if err != nil {
|
|
http.Error(w, "Could not retrieve solution", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
CipherTemplateData.Solution = solution
|
|
}
|
|
|
|
CipherTemplateData.Help = help
|
|
err = CipherTemplate.Execute(w, CipherTemplateData)
|
|
if err != nil {
|
|
http.Error(w, "Could not render template", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func safeRedirectURL(u string) string {
|
|
if strings.HasPrefix(u, "/") && !strings.HasPrefix(u, "//") {
|
|
return u
|
|
}
|
|
return "/team"
|
|
}
|
|
|
|
func main() {
|
|
var err error
|
|
db, err = sql.Open("sqlite3", dbFile+"?_fk=on")
|
|
if err != nil {
|
|
fmt.Println("Error opening database:", err)
|
|
return
|
|
}
|
|
defer db.Close()
|
|
db.SetMaxOpenConns(1)
|
|
|
|
// klice app
|
|
http.HandleFunc("/login", loginHandler)
|
|
http.HandleFunc("/logout", logoutHandler)
|
|
http.HandleFunc("/team", teamInfoHandler)
|
|
http.HandleFunc("/qr/{qr...}", qrHandler)
|
|
// admin app
|
|
http.HandleFunc("/admin/login", adminLoginHandler)
|
|
http.HandleFunc("/admin/logout", adminLogoutHandler)
|
|
http.HandleFunc("/admin/", adminHandler)
|
|
http.HandleFunc("/admin/teams", adminTeamsHandler)
|
|
http.HandleFunc("/admin/start", AdminStartHandler)
|
|
http.HandleFunc("/admin/routes", AdminRouteHandler)
|
|
http.HandleFunc("/admin/levels", AdminLevelHandler)
|
|
http.HandleFunc("/admin/cipher", AdminCipherHandler)
|
|
http.HandleFunc("/admin/positions", AdminPositionsHandler)
|
|
http.HandleFunc("/admin/qr", AdminQRHandler)
|
|
http.HandleFunc("/admin/penalties", AdminPenaltiesHandler)
|
|
|
|
// static files
|
|
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
|
|
|
srv := &http.Server{
|
|
Addr: ":8080",
|
|
Handler: nil,
|
|
ReadTimeout: 10 * time.Second, // zabrání Slowloris útokům
|
|
WriteTimeout: 15 * time.Second, // omezení dlouhých odpovědí
|
|
IdleTimeout: 60 * time.Second, // ukončení nečinných spojení
|
|
MaxHeaderBytes: 1 << 20, // max. 1 MB hlavičky
|
|
}
|
|
|
|
fmt.Println("Server started at :8080")
|
|
srv.ListenAndServe()
|
|
}
|