/*
 * GitTorrent Tracker CGI program -- GTP/0.1
 *
 * Copyright 2006 Jonas Fonseca <fonseca@diku.dk>
 *
 * A simple GitTorrent tracker.
 *
 */

#include <assert.h>
#include <errno.h>
#include <ctype.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>

#include <sys/types.h>
#include <sys/stat.h>

/* Configuration variables */

#define CONFIG_ROOTDIR		"."
#define CONFIG_INTERVAL		900
#define CONFIG_REFERENCES	1
#define CONFIG_PEERS		20
#define CONFIG_UMASK		0600


static char *
sha1_to_hex(const unsigned char *sha1)
{
	static int bufno;
	static char hexbuffer[4][50];
	static const char hex[] = "0123456789abcdef";
	char *buffer = hexbuffer[3 & ++bufno], *buf = buffer;
	int i;

	for (i = 0; i < 20; i++) {
		unsigned int val = *sha1++;
		*buf++ = hex[val >> 4];
		*buf++ = hex[val & 0xf];
	}
	*buf = '\0';

	return buffer;
}

static inline int
unhx(char a)
{
	if (isdigit(a))
		return a - '0';

	if (a >= 'a' && a <= 'f')
		return a - 'a' + 10;

	if (a >= 'A' && a <= 'F')
		return a - 'A' + 10;

	return -1;
}

static void
decode_uri(char *src)
{
	char *dst = src;
	char c;

	while ((c = *src++)) {
		if (c == '%') {
			int x1 = unhx(*src);

			if (x1 >= 0) {
				int x2 = unhx(*(src + 1));

				if (x2 >= 0) {
					x1 = (x1 << 4) + x2;
					if (x1 != 0) { /* don't allow %00 */
						c = (unsigned char) x1;
						src += 2;
					}
				}
			}
		}

		*dst++ = c;
	}

	*dst = 0;
}


static void
print_header(void)
{
	printf(	"HTTP/1.1 200 OK\r\n"
		"Content-Type: application/x-gittorrent\r\n"
		"\r\n");
}

static int
error(const char *err, ...)
{
	char msg[1024];
	va_list args;

	va_start(args, err);
	vsnprintf(msg, sizeof(msg), err, args);
	va_end(args);

	print_header();
	printf("d14:failure reason%d:%se", strlen(msg), msg);

	return -1;
}


/* Environment variable accessor */

static int
check_env_string(const char *var, const char *value)
{
	char *env = getenv(var);

	return env && !strcmp(env, value);
}


/* Repo file accessor */

static ssize_t
get_repo_string(const char *req, char *buf, size_t bufsize)
{
	int fd;

	fd = open(req, O_RDONLY, 0);
	if (!fd)
		return -1;

	return read(fd, buf, bufsize);
}


/* Request string accessors */

static int
get_req_string(const char *req, const char *var, char *buf, size_t bufsize)
{
	const char *pos = req;
	size_t varlen = strlen(var);

	while ((pos = strstr(pos, var))) {
		if (pos[varlen] == '=' &&
		    (pos == req || pos[-1] == '&')) {
			const char *value = pos + varlen + 1;
			const char *end = strchr(value, '&');
			size_t valuelen = end ? end - value : strlen(value);

			if (valuelen > bufsize - 1)
				return 0;

			memcpy(buf, value, valuelen);
			buf[valuelen + 1] = 0;
			decode_uri(buf);

			return 1;
		}

		pos += varlen;
	}

	return 0;
}

static int
get_req_sha1(const char *req, const char *var, char *sha1)
{
	char buf[128];
	char *hex;

	if (!get_req_string(req, var, buf, sizeof(buf)))
		return 0;

	hex = sha1_to_hex((unsigned char *) buf);
	memcpy(sha1, hex, 40);
	sha1[40] = 0;

	return 1;
}

static int
get_req_number(const char *req, const char *var, size_t *value)
{
	char buf[20];

	if (!get_req_string(req, var, buf, sizeof(buf)))
		return 0;

	*value = atol(buf);

	return 1;
}


/* Peer list maintainence */

static void
add_peer(char *peer_id, char *peer_ip, size_t port)
{
	char path[1024];
	char lock[1024];
	FILE *address;

	if (mkdir(peer_id, CONFIG_UMASK) < 0)
		return;

	snprintf(lock, sizeof(lock), "%s/address.lock", peer_id);

	address = fopen(lock, "w");
	if (!address)
		return;

	fprintf(address, "d"
			 "7:peer id20:%.*s"
		         "2:ip%d:%s"
		         "7:porti%de"
		         "e", 20, peer_id, strlen(peer_ip), peer_ip, port);

	snprintf(path, sizeof(path), "%s/address", peer_id);
	rename(lock, path);
}

static int
remove_peer(char *peer_id)
{
	char path[1024];

	snprintf(path, sizeof(path), "%s/address", peer_id);
	unlink(path);

	snprintf(path, sizeof(path), "%s/timestamp", peer_id);
	unlink(path);

	snprintf(path, sizeof(path), "%s/completed", peer_id);
	unlink(path);

	rmdir(peer_id);

	return 0;
}

static void
timestamp_peer(char *peer_id)
{
	char path[1024];
	char lock[1024];
	int fd;

	snprintf(lock, sizeof(lock), "%s/timestamp.lock", peer_id);
	fd = open(lock, O_WRONLY, O_CREAT, CONFIG_UMASK);
	if (fd != -1) {
		snprintf(path, sizeof(path), "%s/timestamp", peer_id);
		rename(lock, path);
	}
}

static int
checkin_peer(char *request)
{
	char peer_id[41];
	char event[32];
	struct stat st;
	size_t port;

	if (!get_req_sha1(request, "peer_id", peer_id))
		return error("No peer ID");

	if (!get_req_number(request, "port", &port))
		return error("No peer port");

	if (get_req_string(request, "event", event, sizeof(event))) {
		char peer_ip[1024];

		if (!strcmp(event, "stopped"))
			return remove_peer(peer_id);

		if (strcmp(event, "started"))
			return error("Unknown event");

		if (!get_req_string(request, "ip", peer_ip, sizeof(peer_ip))) {
			char *addr;

			addr = getenv("REMOTE_HOST");
			if (!addr)
				addr = getenv("REMOTE_ADDR");
			if (!addr)
				return error("No peer address");

			memcpy(peer_ip, addr, strlen(addr) + 1);
		}

		add_peer(peer_id, peer_ip, port);

	} else if (stat(peer_id, &st) < 0) {
		return error("Unknown peer ID");
	}

	timestamp_peer(peer_id);

	return 0;
}


/* Peer list maintainence */

static int
response(size_t references, size_t peers)
{
	char stats[1024];
	ssize_t statssize;

	print_header();
	printf("d");

	printf("8:intervali%de", CONFIG_INTERVAL);

	/* Insert the keys: "incomplete" and "complete" */
	statssize = get_repo_string("stats", stats, sizeof(stats));
	if (statssize > 0)
		fwrite(stats, statssize, 1, stdout);

	printf("5:peersd");
	if (peers) {
		DIR *dir = opendir(".");

		while (dir && peers--) {
			struct dirent *dirent;
			char path[50];
			char address[1024];
			ssize_t addrsize;

			dirent = readdir(dir);
			if (!dirent)
				break;

			if (dirent->d_namlen != 40)
				continue;

			snprintf(path, sizeof(path), "%.*s/address",
				 dirent->d_namlen,  dirent->d_name);

			addrsize = get_repo_string(path, address, sizeof(address));
			if (addrsize > 0)
				fwrite(address, addrsize, 1, stdout);
		}
	}
	printf("e");

	if (references) {
		char refs[10 * 1024];
		ssize_t refssize;

		refssize = get_repo_string("refs", refs, sizeof(refs));
		if (refssize > 0)
			printf("10:referencesl%d:%.*se", refssize, refssize, refs);
	}


	printf("e");

	return 0;
}

int
main(int argc, char *argv[])
{
	char repo_hash[41];
	char *request;
	size_t references, peers;

	if (chdir(CONFIG_ROOTDIR) < 0)
		return error("No tracker root directory");

	if (!check_env_string("REQUEST_METHOD", "GET"))
		return error("Bad request method");

	request = getenv("QUERY_STRING");
	if (!request)
		return error("No reqest");

	if (!get_req_sha1(request, "repo_hash", repo_hash) ||
	    chdir(repo_hash) < 0)
		return error("Unknown repository");

	if (checkin_peer(request) < 0)
		return -1;

	if (!get_req_number(request, "references", &references))
		references = CONFIG_REFERENCES;

	if (!get_req_number(request, "peers", &peers))
		peers = CONFIG_PEERS;

	return response(references, peers);
}

