/*
 * qstat
 * by Steve Jankowski
 *
 * UT2004 master query functions
 * Copyright 2004 Ludwig Nussel
 *
 * Licensed under the Artistic License, see LICENSE.txt for license terms
 *
 * This code is inspired by ideas from 'Nurulwai'
 *
 */

#include <sys/types.h>
#ifndef _WIN32
 #include <sys/socket.h>
#endif
#include <stdlib.h>
#include <stdio.h>

#include "debug.h"
#include "qstat.h"
#include "md5.h"

/** \brief convert bytes into hex string
 *
 * \param in data to convert
 * \param len length of data
 * \param out location to store string to. Must be 2*len
 */
static void bin2hex(const char *in, size_t len, char *out);

#define CD_KEY_LENGTH			23

// arbitrary
#define MAX_LISTING_RECORD_LEN		0x04FF

#define RESPONSE_OFFSET_CDKEY		5
#define RESPONSE_OFFSET_CHALLENGE	39

static const char challenge_response[] =
{
	0x68, 0x00, 0x00, 0x00, '!',  0xCD, 0xCD, 0xCD,
	/* length             |   !   MD5SUM, CD is placeholder */
	0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD,
	0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD,
	0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD,
	0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0x00,
	'!',  0xCD,
	0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD,
	0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD,
	0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD,
	0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0x00,

	0x0c, 'U',  'T',  '2',	'K',  '4',  'C',  'L',
	/*^^ 12 byte string  */
	'I',  'E',  'N',  'T',	0x00, 0xfb, 0x0c, 0x00,
	/*                          | unknown        */
	0x00, 0x06, 0x04, 'i',	'n',  't',  0x00, 0x00,
	/*        |   ^^ 4 byte string          | ?  */
	0x00, 0x00, 0x00, 0xee, 0xee, 0x00, 0x00, 0x11,
	/* unknown                                   */
	0x00, 0x00, 0x00, 0x01
};

static const char approved[] =
{
	0x0e, 0x00, 0x00, 0x00, 0x09, 'A',  'P',  'P',
	'R',  'O',  'V',  'E',	'D',  0x00, 0x03, 0x00,
	0x00, 0x00
};

static const char approved_response[] =
{
	0x22, 0x00, 0x00, 0x00, '!', '0', '0', '0',
	'0',  '0',  '0',  '0',	'0', '0', '0', '0',
	'0',  '0',  '0',  '0',	'0', '0', '0', '0',
	'0',  '0',  '0',  '0',	'0', '0', '0', '0',
	'0',  '0',  '0',  '0',	'0', 0x00
};

static const char verified[] =
{
	0x0a, 0x00, 0x00, 0x00, 0x09, 'V', 'E', 'R',
	'I',  'F',  'I',  'E',	'D', 0x00
};

#if 0
	struct server_listing_record_head {
		unsigned len;
		unsigned ip;
		short port;
		short queryport;
		char name[];
		// char map[]
	};

	struct server_listing_record_foot {
		unsigned char marker1[3];
		unsigned char unknown1;
		unsigned char maxplayers;
		unsigned char unknown2[4];
		unsigned char marker2[3];
	};

#endif

static char cdkey[CD_KEY_LENGTH + 1] = "";

enum ut2004_state {
	STATE_CHALLENGE = 0x00,
	STATE_APPROVED  = 0x01,
	STATE_VERIFIED  = 0x02,
	STATE_LISTING   = 0x03,
};

query_status_t
send_ut2004master_request_packet(struct qserver *server)
{
	int ret;

	if (server->n_packets) {
		return (DONE_FORCE);
	}

	if (!*cdkey) {
		char *param = get_param_value(server, "cdkey", NULL);
		if (!param) {
			debug(0, "Error: missing cdkey parameter");
			server->server_name = SYSERROR;
			return (PKT_ERROR);
		}

		if (strstr(param, "/") != NULL || strstr(param, "\\") != NULL) {
			FILE *fp = fopen(param, "r");
			if (!fp || (fread(cdkey, 1, CD_KEY_LENGTH, fp) != CD_KEY_LENGTH)) {
				debug(0, "Error: can't key from %s", param);
				server->server_name = SYSERROR;
				if (fp) {
					fclose(fp);
				}
				return (PKT_ERROR);
			}
			fclose(fp);
		} else if (strchr(param, '-') && (strlen(param) == CD_KEY_LENGTH)) {
			memcpy(cdkey, param, CD_KEY_LENGTH);
		} else if ((*param == '$') &&
		    (param = getenv(param + 1)) &&                      // replaces param!
		    (strlen(param) == CD_KEY_LENGTH)) {
			memcpy(cdkey, param, CD_KEY_LENGTH);
		} else {
			debug(0, "Error: invalid cdkey parameter");
			server->server_name = SYSERROR;
			return (PKT_ERROR);
		}
	}

	ret = qserver_send(server, NULL, 0);

#if 0
		// XXX since we do not send but rather expect a reply directly after
		// connect it's pointless to retry doing nothing
		debug(0, "retry1: %d", server->retry1);
		server->retry1 = 0;
#endif

	server->master_query_tag[0] = STATE_CHALLENGE;
	return (ret);
}


static void
ut2004_server_done(struct qserver *server)
{
	if (server->saved_data.next) {
		debug(0, "%d bytes of unprocessed data left. Premature EOF!?", server->saved_data.next->datalen);
		free(server->saved_data.next->data);
		free(server->saved_data.next);
		server->saved_data.next = NULL;
	}
}


// we use n_servers to store number of used bytes in master_pkt so
// it needs to be divided by 6 when finished
static void
ut2004_parse_record(struct qserver *server, char *pkt)
{
	char *dest;

#if 0
		unsigned ip;
		unsigned short port;

		memcpy(&ip, pkt + 4, 4);
		port = swap_short_from_little(pkt + 4 + 4);

		debug(2, "got %d.%d.%d.%d:%hu", ip & 0xff, (ip >> 8) & 0xff, (ip >> 16) & 0xff, (ip >> 24) & 0xff, port);
#endif

	if (server->n_servers + 6 > server->master_pkt_len) {
		if (!server->master_pkt_len) {
			server->master_pkt_len = 180;
		} else {
			server->master_pkt_len *= 2;
		}
		server->master_pkt = (char *)realloc(server->master_pkt, server->master_pkt_len);
	}

	dest = server->master_pkt + server->n_servers;

	memcpy(dest, pkt + 4, 4);
	dest[4] = pkt[9];
	dest[5] = pkt[8];
	server->n_servers += 6;
}


static char *
put_bytes(char *buf, const char *bytes, size_t len, size_t *left)
{
	if (!buf || (len > *left)) {
		return (NULL);
	}

	memcpy(buf, bytes, len);
	*left -= len;

	return (buf + len);
}


static char *
put_string(char *buf, const char *string, size_t *left)
{
	size_t len = strlen(string) + 1;
	char l;

	if (!buf || (len > 0xFF) || (*left < len + 1)) {
		return (NULL);
	}

	l = len;

	buf = put_bytes(buf, &l, 1, left);
	return (put_bytes(buf, string, len, left));
}


/** \brief assemble the server filter and send the master query
 *
 * the query consists of four bytes length (excluding the four length bytes), a
 * null byte and then the number of item pairs that follow.
 *
 * Each pair consists of two ut2 strings (length+null terminated string)
 * followed by a byte which is either zero or 0x04 which means negate the query
 * (e.g. not zero curplayers means not empty).
 */
static int
ut2004_send_query(struct qserver *server)
{
	char buf[4096] = { 0 };
	size_t left = sizeof(buf);
	char *b = buf;
	char *param, *r, *sep = "";
	unsigned flen = 0;
	unsigned char items = 0;

	// header is done later
	b += 6;
	left -= 6;

	param = get_param_value(server, "gametype", NULL);
	if (param) {
		++items;
		b = put_string(b, "gametype", &left);
		b = put_string(b, param, &left);
		b = put_bytes(b, "", 1, &left);
	}

	param = get_param_value(server, "status", NULL);
	r = param;
	while (param && sep) {
		sep = strchr(r, ':');
		if (sep) {
			flen = sep - r;
		} else {
			flen = strlen(r);
		}

		if ((strncmp(r, "standard", flen) == 0) ||
		    (strncmp(r, "nostandard", flen) == 0)) {
			++items;
			b = put_string(b, "standard", &left);
			if (*r == 'n') {
				b = put_string(b, "false", &left);
			} else {
				b = put_string(b, "true", &left);
			}
			b = put_bytes(b, "", 1, &left);
		} else if ((strncmp(r, "password", flen) == 0) ||
		    (strncmp(r, "nopassword", flen) == 0)) {
			++items;
			b = put_string(b, "password", &left);
			if (*r == 'n') {
				b = put_string(b, "false", &left);
			} else {
				b = put_string(b, "true", &left);
			}
			b = put_bytes(b, "", 1, &left);
		} else if (strncmp(r, "notempty", flen) == 0) {
			++items;
			b = put_string(b, "currentplayers", &left);
			b = put_string(b, "0", &left);
			b = put_bytes(b, "\x04", 1, &left);
		} else if (strncmp(r, "notfull", flen) == 0) {
			++items;
			b = put_string(b, "freespace", &left);
			b = put_string(b, "0", &left);
			b = put_bytes(b, "\x04", 1, &left);
		} else if (strncmp(r, "nobots", flen) == 0) {
			++items;
			b = put_string(b, "nobots", &left);
			b = put_string(b, "true", &left);
			b = put_bytes(b, "", 1, &left);
		} else if ((strncmp(r, "stats", flen) == 0) ||
		    (strncmp(r, "nostats", flen) == 0)) {
			++items;
			b = put_string(b, "stats", &left);
			if (*r == 'n') {
				b = put_string(b, "false", &left);
			} else {
				b = put_string(b, "true", &left);
			}
			b = put_bytes(b, "", 1, &left);
		} else if ((strncmp(r, "weaponstay", flen) == 0) ||
		    (strncmp(r, "noweaponstay", flen) == 0)) {
			++items;
			b = put_string(b, "weaponstay", &left);
			if (*r == 'n') {
				b = put_string(b, "false", &left);
			} else {
				b = put_string(b, "true", &left);
			}
			b = put_bytes(b, "", 1, &left);
		} else if ((strncmp(r, "transloc", flen) == 0) ||
		    (strncmp(r, "notransloc", flen) == 0)) {
			++items;
			b = put_string(b, "transloc", &left);
			if (*r == 'n') {
				b = put_string(b, "false", &left);
			} else {
				b = put_string(b, "true", &left);
			}
			b = put_bytes(b, "", 1, &left);
		}
		r = sep + 1;
	}

	param = get_param_value(server, "mutator", NULL);
	r = param;
	sep = "";
	while (param && sep) {
		char neg = '\0';
		unsigned char l;
		sep = strchr(r, ':');
		if (sep) {
			flen = sep - r;
		} else {
			flen = strlen(r);
		}

		if (*r == '-') {
			neg = '\x04';
			++r;
			--flen;
		}

		if (!flen) {
			continue;
		}

		b = put_string(b, "mutator", &left);
		l = flen + 1;
		b = put_bytes(b, (char *)&l, 1, &left);
		b = put_bytes(b, r, flen, &left);
		b = put_bytes(b, "", 1, &left);
		b = put_bytes(b, &neg, 1, &left);
		++items;

		r = sep + 1;
	}

	if (!b) {
		debug(0, "Error: query buffer too small. Please file a bug report!");
		return (0);
	}

	put_long_little(b - buf - 4, buf);
	buf[5] = items;

	return (qserver_send(server, buf, sizeof(buf) - left) > 0);
}


query_status_t
deal_with_ut2004master_packet(struct qserver *server, char *rawpkt, int pktlen)
{
	unsigned char *state = (unsigned char *)&server->master_query_tag[0];

	md5_state_t md5;

	if (!pktlen) {
		ut2004_server_done(server);
		goto cleanup_out;
	}

	server->ping_total += time_delta(&packet_recv_time, &server->packet_time1);

	switch (*state) {
	case STATE_CHALLENGE:
		// ensure at least one byte challenge, fit into buffer,
		// match challenge, null terminated
		if ((pktlen < 4 + 1 + 1 + 1) ||
		    (pktlen > 4 + 1 + 8 + 1) ||
		    (rawpkt[pktlen - 1] != '\0')) {
			malformed_packet(server, "invalid challenge");
			goto cleanup_out;
		} else {
			char response[sizeof(challenge_response)];
			char *challenge = rawpkt + 5;
			char sum[16];

			memcpy(response, challenge_response, sizeof(challenge_response));

			debug(2, "challenge: %s", challenge);

			md5_init(&md5);
			md5_append(&md5, (unsigned char *)cdkey, CD_KEY_LENGTH);
			md5_finish(&md5, (unsigned char *)sum);
			bin2hex(sum, 16, response + RESPONSE_OFFSET_CDKEY);

			md5_init(&md5);
			md5_append(&md5, (unsigned char *)cdkey, CD_KEY_LENGTH);
			md5_append(&md5, (unsigned char *)challenge, strlen(challenge));
			md5_finish(&md5, (unsigned char *)sum);
			bin2hex(sum, 16, response + RESPONSE_OFFSET_CHALLENGE);

			if (qserver_send(server, response, sizeof(response))) {
				goto cleanup_out;
			}

			server->server_name = MASTER;

			*state = STATE_APPROVED;
		}
		break;

	case STATE_APPROVED:

		if ((pktlen != sizeof(approved)) ||
		    (0 != memcmp(rawpkt, approved, pktlen))) {
			malformed_packet(server, "CD key not approved");
			goto cleanup_out;
		}

		debug(2, "got approval, sending verify");

		if (qserver_send(server, approved_response, sizeof(approved_response))) {
			goto cleanup_out;
		}

		*state = STATE_VERIFIED;

		break;

	case STATE_VERIFIED:

		if ((pktlen != sizeof(verified)) ||
		    (0 != memcmp(rawpkt, verified, pktlen))) {
			malformed_packet(server, "CD key not verified");
			goto cleanup_out;
		}

		debug(2, "CD key verified, sending query");

		if (ut2004_send_query(server)) {
			goto cleanup_out;
		}

		*state = STATE_LISTING;

		break;

	case STATE_LISTING:
		// first packet. contains number of servers to expect
		if (!server->saved_data.pkt_id) {
			/*
			 *  server->saved_data.data = malloc(pktlen);
			 *  memcpy(server->saved_data.data, rawpkt, pktlen);
			 *  server->saved_data.datalen = pktlen;
			 */
			server->saved_data.pkt_id = 1;

			if (pktlen == 9) {
				unsigned num = swap_long_from_little(rawpkt + 4);
				debug(2, "expect %u servers", num);
#if 1
					if (num < 10000) {
						server->master_pkt_len = num * 6;
						server->master_pkt = (char *)realloc(server->master_pkt, server->master_pkt_len);
					}
#endif
			}
		} else if (pktlen < 4) {
			malformed_packet(server, "packet too short");
			goto cleanup_out;
		} else {
			char *p = rawpkt;
			unsigned recordlen = 0;

			if (server->saved_data.next) {
				unsigned need = 0;
				SavedData *data = server->saved_data.next;
				// nasty, four bytes of record length are split up. since
				// we alloc'ed at least four bytes we just copy the 4-x
				// bytes to data->data
				if (data->datalen < 4) {
					need = 4 - data->datalen;
					debug(2, "need %d bytes more for recordlen", need);
					if (need > pktlen) {
						// XXX ok, im lazy now. Stupid server can't even
						// send four bytes in a row
						malformed_packet(server, "chunk too small");
						goto cleanup_out;
					}
					memcpy(data->data + data->datalen, p, need);
					p += need;
					data->datalen = 4;
				}

				recordlen = swap_long_from_little(data->data);

				if (!recordlen || (recordlen > MAX_LISTING_RECORD_LEN)) {
					malformed_packet(server,
					    "record lengthx %x out of range, position %d", recordlen, (int)(p - rawpkt));
					goto cleanup_out;
				}

				need = 4 + recordlen - data->datalen;

				debug(2, "recordlen: %d, saved: %d, pkglen: %d, needed: %d", recordlen, data->datalen, pktlen, need);

				if (need <= pktlen) {
					data->data = realloc(data->data, 4 + recordlen);
					memcpy(data->data + data->datalen, p, need);
					ut2004_parse_record(server, data->data);
					p += need;

					free(data->data);
					free(data);
					server->saved_data.next = NULL;
				}
			}

			while (!server->saved_data.next && p - rawpkt + 4 < pktlen) {
				recordlen = swap_long_from_little(p);

				// record too large
				if (!recordlen || (recordlen > MAX_LISTING_RECORD_LEN)) {
					malformed_packet(server,
					    "record length %x out of range, position %d", recordlen, (int)(p - rawpkt));
					goto cleanup_out;
				}
				// recordlen itself is four bytes
				recordlen += 4;

				// record fully inside packet
				if (p - rawpkt + recordlen <= pktlen) {
					ut2004_parse_record(server, p);
					p += recordlen;
				} else {
					break;
				}
			}

			// record continues in next packet. save it.
			if (p - rawpkt < pktlen) {
				SavedData *data = server->saved_data.next;
				unsigned tosave = pktlen - (p - rawpkt);
				if (!data) {
					data = malloc(sizeof(SavedData));
					data->data = malloc(tosave < 4 ? 4 : tosave);   // alloc at least four bytes
					data->datalen = tosave;
					memcpy(data->data, p, data->datalen);
					data->next = NULL;
					server->saved_data.next = data;

					debug(1, "saved %d bytes", data->datalen);
				} else {
					data->data = realloc(data->data, data->datalen + tosave);
					memcpy(data->data + data->datalen, p, tosave);
					data->datalen += tosave;

					debug(1, "saved %d bytes (+)", data->datalen);
				}
			}
		}
		break;
	}

	debug(2, "%d servers total", server->n_servers / 6);

	return (INPROGRESS);

cleanup_out:
	server->master_pkt_len = server->n_servers;
	server->n_servers /= 6;
	return (DONE_FORCE);
}


static const char hexchar[] = "0123456789abcdef";

static void
bin2hex(const char *in, size_t len, char *out)
{
	char *o = out + len * 2;

	in += len;
	do {
		*--o = hexchar[*--in & 0x0F];
		*--o = hexchar[(*in >> 4) & 0x0F];
	} while (o != out);
}


// vim: sw=4 ts=4 noet
