moth/src/common.c

449 lines
8.6 KiB
C
Raw Normal View History

2010-09-03 21:00:02 -06:00
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdarg.h>
#include <stdlib.h>
2010-09-03 21:00:02 -06:00
#include <stdio.h>
#include <ctype.h>
#include <values.h>
2010-09-03 21:00:02 -06:00
#include <time.h>
#include "common.h"
/*
* CGI
*/
static int is_cgi = 0;
static char **argv = NULL;
static int
read_char_argv()
{
static int arg = 0;
static char *p;
if (NULL == argv) {
return EOF;
}
if (0 == arg) {
arg = 1;
p = argv[1];
}
if (! p) {
return EOF;
} else if (! *p) {
arg += 1;
p = argv[arg];
return '&';
}
return *(p++);
}
static int
read_char_stdin()
{
static int inlen = -1;
if (-1 == inlen) {
char *p = getenv("CONTENT_LENGTH");
if (p) {
inlen = atoi(p);
} else {
inlen = 0;
}
}
if (inlen) {
inlen -= 1;
return getchar();
}
return EOF;
}
static int
read_char_query_string()
{
static char *p = (char *)-1;
if ((char *)-1 == p) {
p = getenv("QUERY_STRING");
}
if (! p) {
return EOF;
} else if (! *p) {
return EOF;
} else {
return *(p++);
}
}
static int (* read_char)() = read_char_argv;
int
cgi_init(char *global_argv[])
{
char *rm = getenv("REQUEST_METHOD");
if (! rm) {
read_char = read_char_argv;
argv = global_argv;
} else if (0 == strcmp(rm, "POST")) {
read_char = read_char_stdin;
is_cgi = 1;
} else if (0 == strcmp(rm, "GET")) {
read_char = read_char_query_string;
is_cgi = 1;
} else {
printf(("405 Method not allowed\r\n"
"Allow: GET, POST\r\n"
"Content-type: text/plain\r\n"
"\r\n"
"%s is not allowed.\n"),
rm);
return -1;
}
return 0;
}
static char
tonum(int c)
{
if ((c >= '0') && (c <= '9')) {
return c - '0';
}
if ((c >= 'a') && (c <= 'f')) {
return 10 + c - 'a';
}
if ((c >= 'A') && (c <= 'F')) {
return 10 + c - 'A';
}
return 0;
}
static char
read_hex()
{
int a = read_char();
int b = read_char();
return tonum(a)*16 + tonum(b);
}
/* Read a key or a value. Since & and = aren't supposed to appear
outside of boundaries, we can use the same function for both.
*/
size_t
cgi_item(char *str, size_t maxlen)
{
int c;
size_t pos = 0;
while (1) {
c = read_char();
switch (c) {
case EOF:
case '=':
case '&':
str[pos] = '\0';
return pos;
case '%':
c = read_hex();
break;
case '+':
c = ' ';
break;
}
if (pos < maxlen - 1) {
str[pos] = c;
pos += 1;
}
}
}
void
2010-09-12 22:00:58 -06:00
cgi_head(char *title)
{
2010-09-12 22:15:06 -06:00
if (is_cgi) {
printf("Content-type: text/html\r\n\r\n");
}
printf(("<!DOCTYPE html>\n"
"<html>\n"
" <head>\n"
" <title>%s</title>\n"
" <link rel=\"stylesheet\" href=\"ctf.css\" type=\"text/css\">\n"
" </head>\n"
" <body>\n"
" <h1>%s</h1>\n"),
title, title);
2010-09-12 22:00:58 -06:00
}
void
cgi_foot()
{
printf("\n"
" </body>\n"
"</html>\n");
2010-09-12 22:00:58 -06:00
}
void
cgi_page(char *title, char *fmt, ...)
{
va_list ap;
cgi_head(title);
va_start(ap, fmt);
vprintf(fmt, ap);
va_end(ap);
cgi_foot();
exit(0);
}
void
cgi_error(char *fmt, ...)
{
va_list ap;
printf("500 Internal Error\r\n"
"Content-type: text/plain\r\n"
"\r\n");
va_start(ap, fmt);
vprintf(fmt, ap);
va_end(ap);
2010-09-12 22:00:58 -06:00
printf("\n");
exit(0);
}
/*
* Common routines
*/
#define EOL(c) ((EOF == (c)) || (0 == (c)) || ('\n' == (c)))
int
fgrepx(char const *needle, char const *filename)
{
FILE *f;
int found = 0;
char const *p = needle;
f = fopen(filename, "r");
if (f) {
while (1) {
int c = fgetc(f);
/* This list of cases would have looked so much nicer in OCaml. I
apologize. */
if (EOL(c) && (0 == *p)) {
found = 1;
break;
} else if (EOF == c) {
break;
} else if ((0 == p) || (*p != c)) {
p = needle;
do {
c = fgetc(f);
} while (! EOL(c));
} else if ('\n' == c) {
p = needle;
} else {
p += 1;
}
}
fclose(f);
}
return found;
}
2010-09-03 21:00:02 -06:00
int
my_snprintf(char *buf, size_t buflen, char *fmt, ...)
{
int len;
va_list ap;
va_start(ap, fmt);
len = vsnprintf(buf, buflen - 1, fmt, ap);
va_end(ap);
buf[buflen] = '\0';
if (len >= buflen) {
return buflen - 1;
} else {
return len;
}
}
char *
srv_path(char const *fmt, ...)
{
char relpath[PATH_MAX];
static char path[PATH_MAX];
char *srv;
int len;
va_list ap;
va_start(ap, fmt);
len = vsnprintf(relpath, sizeof(relpath) - 1, fmt, ap);
va_end(ap);
relpath[sizeof(relpath) - 1] = '\0';
srv = getenv("CTF_BASE");
if (! srv) {
srv = "/srv/ctf";
}
my_snprintf(path, sizeof(path), "%s/%s", srv, relpath);
return path;
}
int
team_exists(char const *teamhash)
2010-09-03 21:00:02 -06:00
{
struct stat buf;
int ret;
int i;
if ((! teamhash) || (! *teamhash)) {
return 0;
}
2010-09-03 21:00:02 -06:00
/* Check for invalid characters. */
for (i = 0; teamhash[i]; i += 1) {
if (! isalnum(teamhash[i])) {
return 0;
}
}
/* lstat seems to be the preferred way to check for existence. */
ret = lstat(srv_path("teams/names/%s", teamhash), &buf);
2010-09-03 21:00:02 -06:00
if (-1 == ret) {
return 0;
}
return 1;
}
int
award_points(char const *teamhash,
char const *category,
2010-09-12 22:00:58 -06:00
long points)
2010-09-03 21:00:02 -06:00
{
2010-09-06 21:39:59 -06:00
char line[100];
int linelen;
char *filename;
2010-09-06 21:39:59 -06:00
int fd;
int ret;
time_t now = time(NULL);
2010-09-03 21:00:02 -06:00
if (! team_exists(teamhash)) {
return -2;
}
linelen = snprintf(line, sizeof(line),
2010-09-12 22:00:58 -06:00
"%u %s %s %ld\n",
now, teamhash, category, points);
if (sizeof(line) <= linelen) {
2010-09-03 21:00:02 -06:00
return -1;
}
/* At one time I had this writing to a single log file, using lockf.
This works, as long as nobody ever tries to edit the log file.
Editing the log file would require locking it, which would block
everything trying to score, effectively taking down the entire
contest. If you can't lock it first--and nothing in busybox lets
you do this--you have to bring down pretty much everything manually
anyway.
2010-09-03 21:00:02 -06:00
By putting new scores into new files and periodically appending
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
back up, all without affecting things trying to score: they're
still able to record their score and move on. You don't even
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 = srv_path("points.new/%d.%d.%s.%s.%ld",
now, getpid(),
teamhash, category, points);
2010-09-03 21:00:02 -06:00
fd = open(filename, O_WRONLY | O_CREAT | O_EXCL, 0666);
if (-1 == fd) {
2010-09-03 21:00:02 -06:00
return -1;
}
2010-09-07 22:43:24 -06:00
if (-1 == write(fd, line, linelen)) {
close(fd);
return -1;
}
2010-09-03 21:00:02 -06:00
close(fd);
return 0;
}
void
award_and_log_uniquely(char const *team,
char const *category,
2010-09-12 22:00:58 -06:00
long points,
char const *dbfile,
char const *fmt, ...)
{
char *dbpath = srv_path(dbfile);
char line[200];
int len;
int ret;
int fd;
va_list ap;
/* Make sure they haven't already claimed these points */
va_start(ap, fmt);
len = vsnprintf(line, sizeof(line), fmt, ap);
va_end(ap);
if (sizeof(line) <= len) {
cgi_error("Log line too long");
}
if (fgrepx(line, dbpath)) {
cgi_page("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 */
/* We can turn that trailing NUL into a newline now since write
doesn't use C strings */
line[len] = '\n';
lseek(fd, 0, SEEK_END);
if (-1 == write(fd, line, len+1)) {
cgi_error("Unable to append log");
}
close(fd);
}