mirror of https://github.com/dirtbags/moth.git
move mcp to new points architecture
This commit is contained in:
parent
4a3df9a3a2
commit
a2279e94ab
|
@ -1,7 +1,5 @@
|
||||||
#! /bin/sh -e
|
#! /bin/sh -e
|
||||||
|
|
||||||
PATH=/bin:/opt/ctfbase/bin
|
|
||||||
|
|
||||||
if [ $# -ne 1 ]; then
|
if [ $# -ne 1 ]; then
|
||||||
echo "Usage: $0 TEAM" 1>&2
|
echo "Usage: $0 TEAM" 1>&2
|
||||||
exit 64
|
exit 64
|
||||||
|
@ -20,8 +18,8 @@ www=${CTF_BASE:-/var/www}
|
||||||
mkdir -p $base/teams/names
|
mkdir -p $base/teams/names
|
||||||
mkdir -p $base/teams/colors
|
mkdir -p $base/teams/colors
|
||||||
|
|
||||||
[ -f $base/teams/nonce ] || dd if=/dev/urandom count=1 | md5sum | cut -b 1-8 > $base/teams/nonce
|
[ -f $base/teams/salt ] || dd if=/dev/urandom count=1 2>/dev/null | md5sum | cut -b 1-8 > $base/teams/salt
|
||||||
nonce=$(cat $base/teams/nonce)
|
salt=$(cat $base/teams/salt)
|
||||||
|
|
||||||
# Assign a color. I spent weeks selecting a color pallette that
|
# Assign a color. I spent weeks selecting a color pallette that
|
||||||
# wouldn't be hell on people with protanopia. Please don't change these
|
# wouldn't be hell on people with protanopia. Please don't change these
|
||||||
|
@ -48,7 +46,7 @@ esac
|
||||||
# Compute hash of team name; they'll use this for everything in the
|
# Compute hash of team name; they'll use this for everything in the
|
||||||
# contest instead of their team name, which makes stuff much easier on
|
# contest instead of their team name, which makes stuff much easier on
|
||||||
# me since all team hashes are in the set /[0-9a-f]{8}/.
|
# me since all team hashes are in the set /[0-9a-f]{8}/.
|
||||||
hash=$(printf "%s:%s" $nonce "$1" | md5sum | cut -b 1-8)
|
hash=$(printf "%s:%s" $salt "$1" | md5sum | cut -b 1-8)
|
||||||
|
|
||||||
echo "$1" > $base/teams/names/$hash
|
echo "$1" > $base/teams/names/$hash
|
||||||
echo "$color" > $base/teams/colors/$hash
|
echo "$color" > $base/teams/colors/$hash
|
||||||
|
|
|
@ -17,14 +17,6 @@ install -d /var/lib/ctf
|
||||||
install -o ctf -m 0755 -d $NEWDIR
|
install -o ctf -m 0755 -d $NEWDIR
|
||||||
install -o ctf -m 0755 -d /var/lib/ctf/points.tmp
|
install -o ctf -m 0755 -d /var/lib/ctf/points.tmp
|
||||||
|
|
||||||
# Make tokens database now, this is as good a time as any
|
|
||||||
TOKENS=/var/lib/ctf/tokens.db
|
|
||||||
for fn in $TOKENS /opt/*/tokens.txt; do
|
|
||||||
[ -f "$fn" ] || continue
|
|
||||||
cat $fn
|
|
||||||
done | sort | uniq > $TOKENS.new
|
|
||||||
mv $TOKENS.new $TOKENS
|
|
||||||
|
|
||||||
# Create some files
|
# Create some files
|
||||||
CLAIM=/var/lib/ctf/claim.db
|
CLAIM=/var/lib/ctf/claim.db
|
||||||
touch $CLAIM
|
touch $CLAIM
|
||||||
|
|
|
@ -29,9 +29,6 @@ main(int argc, char *argv[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! team_exists(team)) {
|
|
||||||
cgi_result(409, "No such team", "<p>There is no team with that hash.</p>");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Any weird characters in token name? */
|
/* Any weird characters in token name? */
|
||||||
{
|
{
|
||||||
|
@ -49,12 +46,6 @@ main(int argc, char *argv[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Does the token exist? */
|
|
||||||
if (! fgrepx(token, state_path("tokens.db"))) {
|
|
||||||
cgi_result(409, "No such token", "<p>This token has not been issued.</p>");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Award points */
|
/* Award points */
|
||||||
{
|
{
|
||||||
char *p = token;
|
char *p = token;
|
||||||
|
@ -62,6 +53,7 @@ main(int argc, char *argv[])
|
||||||
char category[40];
|
char category[40];
|
||||||
char points_s[40];
|
char points_s[40];
|
||||||
int points;
|
int points;
|
||||||
|
int ret;
|
||||||
|
|
||||||
/* Pull category name out of the token */
|
/* Pull category name out of the token */
|
||||||
for (q = category; *p && (*p != ':'); p += 1) {
|
for (q = category; *p && (*p != ':'); p += 1) {
|
||||||
|
@ -70,6 +62,11 @@ main(int argc, char *argv[])
|
||||||
*q = '\0';
|
*q = '\0';
|
||||||
if (p) p += 1;
|
if (p) p += 1;
|
||||||
|
|
||||||
|
/* Now we can check to see if it's valid */
|
||||||
|
if (! anchored_search(package_path("%s/tokens.txt", category), token, 0)) {
|
||||||
|
cgi_result(409, "No such token", "<p>This token has not been issued.</p>");
|
||||||
|
}
|
||||||
|
|
||||||
/* Pull point value out of the token (if it has one) */
|
/* Pull point value out of the token (if it has one) */
|
||||||
for (q = points_s; *p && (*p != ':'); p += 1) {
|
for (q = points_s; *p && (*p != ':'); p += 1) {
|
||||||
*(q++) = *p;
|
*(q++) = *p;
|
||||||
|
@ -78,11 +75,9 @@ main(int argc, char *argv[])
|
||||||
points = atoi(points_s);
|
points = atoi(points_s);
|
||||||
if (0 == points) points = 1;
|
if (0 == points) points = 1;
|
||||||
|
|
||||||
{
|
ret = award_points(team, category, points, token);
|
||||||
char line[200];
|
if (ret < 0) {
|
||||||
|
cgi_fail(ret);
|
||||||
my_snprintf(line, sizeof(line), "%s %s", team, token);
|
|
||||||
award_and_log_uniquely(team, category, points, state_path("claim.db"), line);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -228,6 +228,21 @@ cgi_result(int code, char *desc, char *fmt, ...)
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
cgi_fail(int err)
|
||||||
|
{
|
||||||
|
switch (err) {
|
||||||
|
case ERR_GENERAL:
|
||||||
|
cgi_result(500, "Points not awarded", "<p>The server is unable to award your points at this time.</p>");
|
||||||
|
case ERR_NOTEAM:
|
||||||
|
cgi_result(409, "No such team", "<p>There is no team with that hash.</p>");
|
||||||
|
case ERR_CLAIMED:
|
||||||
|
cgi_result(409, "Already claimed", "<p>Your team has already claimed these points.</p>");
|
||||||
|
default:
|
||||||
|
cgi_result(409, "Failure", "<p>Failure code: %d</p>", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
cgi_page(char *title, char *fmt, ...)
|
cgi_page(char *title, char *fmt, ...)
|
||||||
{
|
{
|
||||||
|
@ -252,44 +267,55 @@ cgi_error(char *text)
|
||||||
* Common routines
|
* Common routines
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* cut -d$ANCHOR -f2- | grep -Fx "$NEEDLE" */
|
||||||
#define EOL(c) ((EOF == (c)) || ('\n' == (c)))
|
|
||||||
|
|
||||||
int
|
int
|
||||||
fgrepx(char const *needle, char const *filename)
|
anchored_search(char const *filename, char const *needle, char const anchor)
|
||||||
{
|
{
|
||||||
FILE *f;
|
FILE *f = fopen(filename, "r");
|
||||||
int found = 0;
|
size_t nlen = strlen(needle);
|
||||||
char const *p = needle;
|
char line[1024];
|
||||||
|
int ret = 0;
|
||||||
|
|
||||||
f = fopen(filename, "r");
|
while (f) {
|
||||||
if (f) {
|
char *p;
|
||||||
while (1) {
|
|
||||||
int c = fgetc(f);
|
|
||||||
|
|
||||||
/* This list of cases would have looked so much nicer in OCaml. I
|
if (NULL == fgets(line, sizeof line, f)) {
|
||||||
apologize. */
|
break;
|
||||||
if (EOL(c) && ('\0' == *p)) {
|
}
|
||||||
found = 1;
|
|
||||||
break;
|
/* Find anchor */
|
||||||
} else if (EOF == c) { /* End of file */
|
if (anchor) {
|
||||||
break;
|
p = strchr(line, anchor);
|
||||||
} else if (('\0' == p) || (*p != (char)c)) {
|
if (! p) {
|
||||||
p = needle;
|
continue;
|
||||||
/* Discard the rest of the line */
|
}
|
||||||
do {
|
p += 1;
|
||||||
c = fgetc(f);
|
} else {
|
||||||
} while (! EOL(c));
|
p = line;
|
||||||
} else if (EOL(c)) {
|
}
|
||||||
p = needle;
|
|
||||||
} else { /* It matched */
|
/* Don't bother with strcmp if string lengths differ.
|
||||||
p += 1;
|
If this string is shorter than the previous, it's okay. This is
|
||||||
}
|
just a performance hack.
|
||||||
|
*/
|
||||||
|
if ((p[nlen] != '\n') &&
|
||||||
|
(p[nlen] != '\0')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
p[nlen] = 0;
|
||||||
|
|
||||||
|
/* Okay, now we can compare! */
|
||||||
|
if (0 == strcmp(p, needle)) {
|
||||||
|
ret = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
return found;
|
if (f) {
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -343,9 +369,8 @@ mkpath(char const *base, char const *fmt, va_list ap)
|
||||||
char relpath[PATH_MAX];
|
char relpath[PATH_MAX];
|
||||||
static char path[PATH_MAX];
|
static char path[PATH_MAX];
|
||||||
char const *var;
|
char const *var;
|
||||||
int len;
|
|
||||||
|
|
||||||
len = vsnprintf(relpath, sizeof(relpath) - 1, fmt, ap);
|
vsnprintf(relpath, sizeof(relpath) - 1, fmt, ap);
|
||||||
relpath[sizeof(relpath) - 1] = '\0';
|
relpath[sizeof(relpath) - 1] = '\0';
|
||||||
|
|
||||||
var = getenv("CTF_BASE");
|
var = getenv("CTF_BASE");
|
||||||
|
@ -409,26 +434,36 @@ team_exists(char const *teamhash)
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Return values:
|
||||||
|
-1: general error
|
||||||
|
-2: no such team
|
||||||
|
-3: points already awarded
|
||||||
|
*/
|
||||||
int
|
int
|
||||||
award_points(char const *teamhash,
|
award_points(char const *teamhash,
|
||||||
char const *category,
|
char const *category,
|
||||||
long points)
|
const long points,
|
||||||
|
char const *uid)
|
||||||
{
|
{
|
||||||
char line[100];
|
char line[100];
|
||||||
int linelen;
|
int linelen;
|
||||||
char *filename;
|
char *filename;
|
||||||
int fd;
|
FILE *f;
|
||||||
time_t now = time(NULL);
|
time_t now = time(NULL);
|
||||||
|
|
||||||
if (! team_exists(teamhash)) {
|
if (! team_exists(teamhash)) {
|
||||||
return -2;
|
return ERR_NOTEAM;
|
||||||
}
|
}
|
||||||
|
|
||||||
linelen = snprintf(line, sizeof(line),
|
linelen = snprintf(line, sizeof(line),
|
||||||
"%lu %s %s %ld\n",
|
"%s %s %ld %s",
|
||||||
(unsigned long)now, teamhash, category, points);
|
teamhash, category, points, uid);
|
||||||
if (sizeof(line) <= linelen) {
|
if (sizeof(line) <= linelen) {
|
||||||
return -1;
|
return ERR_GENERAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anchored_search(state_path("points.log"), line, ' ')) {
|
||||||
|
return ERR_CLAIMED;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* At one time I had this writing to a single log file, using lockf.
|
/* At one time I had this writing to a single log file, using lockf.
|
||||||
|
@ -443,43 +478,30 @@ award_points(char const *teamhash,
|
||||||
those files to the main log file, it is possible to stop the thing
|
those files to the main log file, it is possible to stop the thing
|
||||||
that appends, edit the file at leisure, and then start the appender
|
that appends, edit the file at leisure, and then start the appender
|
||||||
back up, all without affecting things trying to score: they're
|
back up, all without affecting things trying to score: they're
|
||||||
still able to record their score and move on. You don't even
|
still able to record their score and move on.
|
||||||
really need an appender, but it does make things look a little
|
|
||||||
nicer on the fs.
|
|
||||||
|
|
||||||
The fact that this makes the code simpler is just gravy.
|
|
||||||
|
|
||||||
Note that doing this means there's a little time between when a
|
|
||||||
score's written and when the scoreboard (which only reads the log
|
|
||||||
file) picks it up. It's not a big deal for the points log, but
|
|
||||||
this situation makes this technique unsuitable for writing log
|
|
||||||
files that prevent people from double-scoring, like the puzzler or
|
|
||||||
token log.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
filename = state_path("points.tmp/%d.%d.%s.%s.%ld",
|
filename = state_path("points.tmp/%lu.%d.%s.%s.%ld",
|
||||||
now, getpid(),
|
(unsigned long)now, getpid(),
|
||||||
teamhash, category, points);
|
teamhash, category, points);
|
||||||
|
f = fopen(filename, "w");
|
||||||
fd = open(filename, O_WRONLY | O_CREAT | O_EXCL, 0666);
|
if (! f) {
|
||||||
if (-1 == fd) {
|
return ERR_GENERAL;
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-1 == write(fd, line, linelen)) {
|
if (EOF == fprintf(f, "%lu %s\n", (unsigned long)now, line)) {
|
||||||
close(fd);
|
return ERR_GENERAL;
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
close(fd);
|
fclose(f);
|
||||||
|
|
||||||
/* Rename into points.new */
|
/* Rename into points.new */
|
||||||
{
|
{
|
||||||
char ofn[PATH_MAX];
|
char ofn[PATH_MAX];
|
||||||
|
|
||||||
strncpy(ofn, filename, sizeof(ofn));
|
strncpy(ofn, filename, sizeof(ofn));
|
||||||
filename = state_path("points.new/%d.%d.%s.%s.%ld",
|
filename = state_path("points.new/%lu.%d.%s.%s.%ld",
|
||||||
now, getpid(),
|
(unsigned long)now, getpid(),
|
||||||
teamhash, category, points);
|
teamhash, category, points);
|
||||||
rename(ofn, filename);
|
rename(ofn, filename);
|
||||||
}
|
}
|
||||||
|
@ -487,49 +509,3 @@ award_points(char const *teamhash,
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Award points iff they haven't been logged.
|
|
||||||
|
|
||||||
If [line] is not in [dbfile], append it and give [points] to [team]
|
|
||||||
in [category].
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
award_and_log_uniquely(char const *team,
|
|
||||||
char const *category,
|
|
||||||
long points,
|
|
||||||
char const *dbpath,
|
|
||||||
char const *line)
|
|
||||||
{
|
|
||||||
int fd;
|
|
||||||
|
|
||||||
/* Make sure they haven't already claimed these points */
|
|
||||||
if (fgrepx(line, dbpath)) {
|
|
||||||
cgi_result(409, "Already claimed",
|
|
||||||
"<p>Your team has already claimed these points.</p>");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Open and lock logfile */
|
|
||||||
fd = open(dbpath, O_WRONLY | O_CREAT, 0666);
|
|
||||||
if (-1 == fd) {
|
|
||||||
cgi_error("Unable to open log");
|
|
||||||
}
|
|
||||||
if (-1 == lockf(fd, F_LOCK, 0)) {
|
|
||||||
cgi_error("Unable to lock log");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Award points */
|
|
||||||
if (0 != award_points(team, category, points)) {
|
|
||||||
cgi_error("Unable to award points");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Log that we did so */
|
|
||||||
lseek(fd, 0, SEEK_END);
|
|
||||||
if (-1 == write(fd, line, strlen(line))) {
|
|
||||||
cgi_error("Unable to append log");
|
|
||||||
}
|
|
||||||
if (-1 == write(fd, "\n", 1)) {
|
|
||||||
cgi_error("Unable to append log");
|
|
||||||
}
|
|
||||||
close(fd);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,28 +9,29 @@
|
||||||
#define TOKEN_MAX 80
|
#define TOKEN_MAX 80
|
||||||
#define itokenlen 5
|
#define itokenlen 5
|
||||||
|
|
||||||
|
#define ERR_GENERAL -1
|
||||||
|
#define ERR_NOTEAM -2
|
||||||
|
#define ERR_CLAIMED -3
|
||||||
|
|
||||||
int cgi_init(char *global_argv[]);
|
int cgi_init(char *global_argv[]);
|
||||||
size_t cgi_item(char *str, size_t maxlen);
|
size_t cgi_item(char *str, size_t maxlen);
|
||||||
void cgi_head(char *title);
|
void cgi_head(char *title);
|
||||||
void cgi_foot();
|
void cgi_foot();
|
||||||
void cgi_result(int code, char *desc, char *fmt, ...);
|
void cgi_result(int code, char *desc, char *fmt, ...);
|
||||||
|
void cgi_fail(int err);
|
||||||
void cgi_page(char *title, char *fmt, ...);
|
void cgi_page(char *title, char *fmt, ...);
|
||||||
void cgi_error(char *text);
|
void cgi_error(char *text);
|
||||||
|
|
||||||
|
|
||||||
int fgrepx(char const *needle, char const *filename);
|
int anchored_search(char const *filename, char const *needle, const char anchor);
|
||||||
void urandom(char *buf, size_t buflen);
|
void urandom(char *buf, size_t buflen);
|
||||||
int my_snprintf(char *buf, size_t buflen, char *fmt, ...);
|
int my_snprintf(char *buf, size_t buflen, char *fmt, ...);
|
||||||
char *state_path(char const *fmt, ...);
|
char *state_path(char const *fmt, ...);
|
||||||
char *package_path(char const *fmt, ...);
|
char *package_path(char const *fmt, ...);
|
||||||
int team_exists(char const *teamhash);
|
int team_exists(char const *teamhash);
|
||||||
int award_points(char const *teamhacsh,
|
int award_points(char const *teamhash,
|
||||||
char const *category,
|
char const *category,
|
||||||
long point);
|
long point,
|
||||||
void award_and_log_uniquely(char const *team,
|
char const *uid);
|
||||||
char const *category,
|
|
||||||
long points,
|
|
||||||
char const *logfile,
|
|
||||||
char const *line);
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <sysexits.h>
|
#include <sysexits.h>
|
||||||
|
#include <time.h>
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
int
|
int
|
||||||
|
@ -8,9 +9,10 @@ main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
int points;
|
int points;
|
||||||
int ret;
|
int ret;
|
||||||
|
char comment[512];
|
||||||
|
|
||||||
if (argc != 4) {
|
if (argc != 5) {
|
||||||
fprintf(stderr, "Usage: pointscli TEAM CATEGORY POINTS\n");
|
fprintf(stderr, "Usage: pointscli TEAM CATEGORY POINTS 'COMMENT'\n");
|
||||||
return EX_USAGE;
|
return EX_USAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,17 +22,22 @@ main(int argc, char *argv[])
|
||||||
return EX_USAGE;
|
return EX_USAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = award_points(argv[1], argv[2], points);
|
snprintf(comment, sizeof comment, "--%s", argv[4]);
|
||||||
|
|
||||||
|
ret = award_points(argv[1], argv[2], points, comment);
|
||||||
switch (ret) {
|
switch (ret) {
|
||||||
case -3:
|
case ERR_GENERAL:
|
||||||
fprintf(stderr, "Runtime error\n");
|
perror("General error");
|
||||||
return EX_SOFTWARE;
|
return EX_UNAVAILABLE;
|
||||||
case -2:
|
case ERR_NOTEAM:
|
||||||
fprintf(stderr, "No such team\n");
|
fprintf(stderr, "No such team\n");
|
||||||
return EX_NOUSER;
|
return EX_NOUSER;
|
||||||
case -1:
|
case ERR_CLAIMED:
|
||||||
perror("Couldn't award points");
|
fprintf(stderr, "Duplicate entry\n");
|
||||||
return EX_UNAVAILABLE;
|
return EX_DATAERR;
|
||||||
|
default:
|
||||||
|
fprintf(stderr, "Error %d\n", ret);
|
||||||
|
return EX_SOFTWARE;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
@ -40,11 +40,6 @@ main(int argc, char *argv[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Check to see if team exists */
|
|
||||||
if (! team_exists(team)) {
|
|
||||||
cgi_page("No such team", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Validate category name (prevent directory traversal) */
|
/* Validate category name (prevent directory traversal) */
|
||||||
{
|
{
|
||||||
char *p;
|
char *p;
|
||||||
|
@ -62,19 +57,16 @@ main(int argc, char *argv[])
|
||||||
|
|
||||||
my_snprintf(needle, sizeof(needle), "%ld %s", points, answer);
|
my_snprintf(needle, sizeof(needle), "%ld %s", points, answer);
|
||||||
|
|
||||||
if (! fgrepx(needle,
|
if (! anchored_search(package_path("%s/answers.txt", category), needle, 0)) {
|
||||||
package_path("%s/answers.txt", category))) {
|
|
||||||
cgi_page("Wrong answer", "");
|
cgi_page("Wrong answer", "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
char line[200];
|
int ret = award_points(team, category, points, "P");
|
||||||
|
if (ret < 0) {
|
||||||
my_snprintf(line, sizeof(line),
|
cgi_fail(ret);
|
||||||
"%s %s %ld", team, category, points);
|
}
|
||||||
award_and_log_uniquely(team, category, points,
|
|
||||||
state_path("puzzles.db"), line);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cgi_page("Points awarded",
|
cgi_page("Points awarded",
|
||||||
|
|
Loading…
Reference in New Issue