1stMUD4.0/bin/
1stMUD4.0/doc/MPDocs/
1stMUD4.0/player/
1stMUD4.0/win32/
1stMUD4.0/win32/rom/
/**************************************************************************/
// sendstat.c - provide statistics on 1stMUD based muds to 1stmud.dlmud.com
/***************************************************************************
 * The Dawn of Time v1.69q (c)1997-2002 Michael Garratt                    *
 * >> A number of people have contributed to the Dawn codebase, with the   *
 *    majority of code written by Michael Garratt - www.dawnoftime.org     *
 * >> To use this source code, you must fully comply with the dawn license *
 *    in licenses.txt... In particular, you may not remove this copyright  *
 *    notice.                                                              *
 **************************************************************************/
// About: The code within this module sends to 1stmud.dlmud.com statistical
//        information.  Using this information I am able to 
//        get an idea of the number of 1stmud based muds running and 
//        hopefully the number of players.
//      
//        The statistical information submitted to 1stmud.dlmud.com is a 
//        summary of mudstats, combined 
//        with a few other things specific to your mud environment 
//        (such as the name of the mud).  All information is kept private
//        and not made publically available through 1stmud.dlmud.com... but
//        in the future 1stmud.dlmud.com may include the statistical 
//        information to report how many 1stmud based muds are running, how
//        popular various mud clients are etc.
//
//        This information may be made publically available at
//        either the 1stmud.dlmud.com website or 
//        http://1stmud.dlmud.com/muds in the future (once enough 
//        muds are running 1stmud to make it worth while).
//      
//        By default the mud after about 10 minutes of running will 
//        send the stats to http://1stmud.dlmud.com/cgi-bin/1stmud/post.cgi
//        This does not lag the mud in anyway, (unless your dns resolver
//        is broken - use sockets to determine this), if the dns resolver 
//        is broken there may be a one off small delay (ordinarily less 
//        than 5 seconds) while the mud resolves the ip address of 
//        1stmud.dlmud.com in order to know where to send the 
//        stats to.
// 
// 
#include "merc.h"
#include "recycle.h"
#include "globals.h"
#include "webserver.h"
#include "tables.h"
#include "olc.h"				//flag_string()

//#define SENDSTAT_LOG_PROGRESS
#define SENDSTAT_SUBMIT_DOMAIN "1stmud.dlmud.com"
#define SENDSTAT_SUBMIT_URL "/cgi-bin/1stmud/post.cgi"

void sendstat_logf(const char *fmt, ...)
{
	char buf[MSL];
	va_list args;
#if !defined(SENDSTAT_LOG_PROGRESS)
	return;
#endif
	if (IS_NULLSTR(fmt))
		return;

	va_start(args, fmt);
	vsnprintf(buf, MSL, fmt, args);
	va_end(args);

	logf("sendstat: %s", buf);
}

int count_player_list(void)
{
	int count = 0;
	CHAR_DATA *ch = player_first;
	for (; ch; ch = ch->next_player)
	{
		count++;
	}
	return count;
}

const char *url_encode_table[] = {	// as per RFC1728
	"%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07",
	"%08", "%09", "%0A", "%0B", "%0C", "%0D", "%0E", "%0F",
	"%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17",
	"%18", "%19", "%1A", "%1B", "%1C", "%1D", "%1E", "%1F",
	"+", "!", "%22", "%23", "$", "%25", "%26", "%27",
	"(", ")", "*", "%2B", ",", "-", ".", "%2F",
	"0", "1", "2", "3", "4", "5", "6", "7",
	"8", "9", "%3A", "%3B", "%3C", "%3D", "%3E", "%3F",
	"%40", "A", "B", "C", "D", "E", "F", "G",
	"H", "I", "J", "K", "L", "M", "N", "O",
	"P", "Q", "R", "S", "T", "U", "V", "W",
	"X", "Y", "Z", "%5B", "%5C", "%5D", "%5E", "_",
	"%60", "a", "b", "c", "d", "e", "f", "g",
	"h", "i", "j", "k", "l", "m", "n", "o",
	"p", "q", "r", "s", "t", "u", "v", "w",
	"x", "y", "z", "%7B", "%7C", "%7D", "%7E", "%7F",
	"%80", "%81", "%82", "%83", "%84", "%85", "%86", "%87",
	"%88", "%89", "%8A", "%8B", "%8C", "%8D", "%8E", "%8F",
	"%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97",
	"%98", "%99", "%9A", "%9B", "%9C", "%9D", "%9E", "%9F",
	"%A0", "%A1", "%A2", "%A3", "%A4", "%A5", "%A6", "%A7",
	"%A8", "%A9", "%AA", "%AB", "%AC", "%AD", "%AE", "%AF",
	"%B0", "%B1", "%B2", "%B3", "%B4", "%B5", "%B6", "%B7",
	"%B8", "%B9", "%BA", "%BB", "%BC", "%BD", "%BE", "%BF",
	"%C0", "%C1", "%C2", "%C3", "%C4", "%C5", "%C6", "%C7",
	"%C8", "%C9", "%CA", "%CB", "%CC", "%CD", "%CE", "%CF",
	"%D0", "%D1", "%D2", "%D3", "%D4", "%D5", "%D6", "%D7",
	"%D8", "%D9", "%DA", "%DB", "%DC", "%DD", "%DE", "%DF",
	"%E0", "%E1", "%E2", "%E3", "%E4", "%E5", "%E6", "%E7",
	"%E8", "%E9", "%EA", "%EB", "%EC", "%ED", "%EE", "%EF",
	"%F0", "%F1", "%F2", "%F3", "%F4", "%F5", "%F6", "%F7",
	"%F8", "%F9", "%FA", "%FB", "%FC", "%FD", "%FE", "%FF"
};

char *url_encode_post_data(char *postdata)
{
	static char *result;
	unsigned char *s;			// src
	const char *t;				// text
	char *d;

	// allocate the maximum length the given post could possibly take
	if (result)
	{
		free_mem(result);
	}
	alloc_mem(result, char, strlen(postdata) * 3 + 1);
	d = result;					// dest

	for (s = (unsigned char *) postdata; *s; s++)
	{
		t = url_encode_table[*s];
		while (*t)
		{
			*d++ = *t++;
		}
	}
	*d = NUL;					// terminate the result
	return result;
}

char *sendstat_generate_statistics_text()
{
	static char result[45000];
	char stats[45000];
	char *encoded;

	stats[0] = NUL;

#define ENCODE_INT(field) strcat(stats, FORMATF("&" # field"=%d", field))
#define ENCODE_INTH(field, header) strcat(stats, FORMATF("&" header"=%d", field))
#define ENCODE_STR(field) strcat(stats, FORMATF("&" # field"=%s", url_encode_post_data(field)))
#define ENCODE_STRH(field, header) strcat(stats, FORMATF("&" header"=%s", url_encode_post_data(field)))
#define ENCODE_TIME(field) strcat(stats, FORMATF("&" # field"=%s", url_encode_post_data(str_time(field, -1, NULL))))

	ENCODE_STRH(MUD_NAME, "name");
	ENCODE_INT(port);
#if !defined(NO_WEB)
	ENCODE_INTH(WEBSERVERPORT, "webport");
#endif
	ENCODE_INT(top_area);
	ENCODE_INT(top_room);
	ENCODE_INT(top_shop);
	ENCODE_INT(top_mob_index);
	ENCODE_INT(mobile_count);
	ENCODE_INT(top_obj_index);
	ENCODE_INT(top_help);
	ENCODE_INT(LEVEL_IMMORTAL);
	ENCODE_INT(MAX_LEVEL);
	ENCODE_TIME(current_time);
	ENCODE_INT(maxRace);
	ENCODE_INT(maxClass);
	ENCODE_INT(maxSkill);
	ENCODE_INT(maxGroup);
	ENCODE_INT(maxSocial);
	ENCODE_INT(maxClan);
	ENCODE_INT(maxCommands);
	ENCODE_INT(maxDeity);
	ENCODE_STR(HOSTNAME);
	ENCODE_INT(maxChannel);
	ENCODE_STRH((char *) flag_string(mud_flags, mud_info.mud_flags),
				"mudflags");
	ENCODE_STRH(str_time(boot_time, -1, NULL), "boot_time");
#if defined(__cplusplus)
	ENCODE_STRH("yes", "Cplus");
#endif
#if !defined(NO_MCCP)
	ENCODE_STRH("yes", "MCCP");
#endif
	ENCODE_INT(MAX_KEY_HASH);
	ENCODE_INT(MAX_STRING_LENGTH);
	ENCODE_INT(MAX_INPUT_LENGTH);
	ENCODE_INTH(count_player_list(), "current_player_count");

#if defined(__DATE__)
	ENCODE_STRH(__DATE__, "compiled_date");
#endif
#if defined(__TIME__)
	ENCODE_STRH(__TIME__, "compiled_time");
#endif
#if defined (__CYGWIN__)
	ENCODE_STRH("cygwin", "compiled_platform");
#elif defined (WIN32)
	ENCODE_STRH("Win32", "compiled_platform");
#elif defined (unix)
#	if defined (linux)
	ENCODE_STRH("linux", "compiled_platform");
#	elif defined (__OpenBSD__)
	ENCODE_STRH("OpenBSD", "compiled_platform");
#	elif defined (__FreeBSD__)
	ENCODE_STRH("FreeBSD", "compiled_platform");
#	elif defined (__NetBSD__)
	ENCODE_STRH("NetBSD", "compiled_platform");
#	elif defined(BSD)
	ENCODE_STRH("BSD", "compiled_platform");
#	else
	ENCODE_STRH("unix", "compiled_platform");
#	endif
#elif
	ENCODE_STRH("unknown", "compiled_platform");
#endif
	// possible compiler thing of interest
	// e.g. "2.96 20000731 (Red Hat Linux 7.1 2.96-85)"
#if defined(__VERSION__)
	ENCODE_STRH(__VERSION__, "compiler_version");
#endif

	encoded = &stats[1];
	sprintf(result,
			"POST " SENDSTAT_SUBMIT_URL "  HTTP/1.1\r\n"
			"Content-Type: application/x-www-form-urlencoded\r\n"
			"User-Agent: Mozilla/4.0 (compatible; 1stMUDWebSubmit/1.0)\r\n"
			"Host: " SENDSTAT_SUBMIT_DOMAIN "\r\n"
			"Content-Length: %d\r\n"
			"Cache-Control: no-cache\r\n" "\r\n%s", strlen(encoded), encoded);

	return result;
}

char *sendstat_stattext_to_post;

typedef enum
{
	SENDSTATSTAGE_WAIT,
	SENDSTATSTAGE_DOMAIN_RESOLVED,
	SENDSTATSTAGE_CONNECT_IN_PROGRESS,
	SENDSTATSTAGE_GENERATE_STATS,
	SENDSTATSTAGE_POSTING,
	SENDSTATSTAGE_CLOSE_int,
	SENDSTATSTAGE_COMPLETED,
	SENDSTATSTAGE_ABORTED
}
sendstat_stages;

sendstat_stages sendstat_stage = SENDSTATSTAGE_WAIT;
time_t sendstat_connect_timeout = 0;

struct in_addr sendstat_address;
static int sendstat_socket;

// - resolve domain name, performed once per reboot
void sendstat_resolve_domain()
{
	// resolve domain name
	struct hostent *h = NULL;
	if (sendstat_address.s_addr == 0)
	{

		h = gethostbyname(SENDSTAT_SUBMIT_DOMAIN ".");
		// note: putting the . on the end of the domain name above 
		//       to ensure we don't try and resolve a subdomain
		if (!h)
		{
			// if we get NULL it failed to resolve
			sendstat_logf("failed to resolve '%s.'", SENDSTAT_SUBMIT_DOMAIN);
			sendstat_stage = SENDSTATSTAGE_ABORTED;
			return;
		}
		// ip address
		if (h && h->h_addr_list[0])
		{
			memcpy(&sendstat_address.s_addr, h->h_addr_list[0], h->h_length);
		}
	}

	sendstat_logf("resolved '%s' as %s",
				  SENDSTAT_SUBMIT_DOMAIN, inet_ntoa(sendstat_address));
	sendstat_stage = SENDSTATSTAGE_DOMAIN_RESOLVED;
}

struct sockaddr_in sockaddress;

void sendstat_initiate_connection()
{
	//  initiate connection to http://1stmud.dlmud.com
	int nRet;

	// create a socket 
	sendstat_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sendstat_socket == -1)
	{
		sendstat_logf("Error creating connection socket");
		return;
	}

	// setup the socket address structure for where we want to connect to
	sockaddress.sin_family = AF_INET;
	sockaddress.sin_addr.s_addr = sendstat_address.s_addr;
	sockaddress.sin_port = htons(80);

	{							// set the socket to non-blocking mode
#if defined(WIN32)
		unsigned long blockmode = 1;
		if (ioctlsocket(sendstat_socket, FIONBIO, &blockmode) != 0)
		{
			sendstat_logf
				("ioctlsocket: error setting new socket to nonblocking, "
				 "WSAGetLastError=%d", WSAGetLastError());
			sendstat_stage = SENDSTATSTAGE_ABORTED;
			return;
		}
#else
		if (fcntl(sendstat_socket, F_SETFL, O_NONBLOCK) < 0)
		{
			sendstat_logf("fcntl: error setting new socket to nonblocking");
			sendstat_stage = SENDSTATSTAGE_ABORTED;
			return;
		}
#endif
	}

	// start the connection 
	nRet = connect(sendstat_socket, (struct sockaddr *) &sockaddress,
				   sizeof(struct sockaddr_in));

	// check for instant results

	if (nRet == 0)
	{							// successful connection, jump straight to generating the stats to post
		sendstat_stage = SENDSTATSTAGE_GENERATE_STATS;
		return;
	}

	// check the error codes, if we have anything other than 
	// what is normal for a blocked connect call, we abort the connection
#if defined(WIN32)
	if (WSAGetLastError() != WSAEWOULDBLOCK)
	{
		sendstat_logf("sendstat_initiate_connection(): connect() error %d",
					  WSAGetLastError());
		sendstat_stage = SENDSTATSTAGE_ABORTED;
		return;
	}
#else
	if (nRet < 0)
	{
		if (errno != EINPROGRESS && errno != EALREADY)
		{
			sendstat_logf("sendstat_initiate_connection(): connect() error %d",
						  errno);
			sendstat_stage = SENDSTATSTAGE_ABORTED;
			return;
		}
	}
#endif

	// the connection process has started, and is now in progress
	sendstat_connect_timeout = current_time + 200;	// 200 seconds to connect
	sendstat_logf("connection initiation successful");
	sendstat_stage = SENDSTATSTAGE_CONNECT_IN_PROGRESS;
	return;
}

void sendstat_process_connect()
{
	int nRet;
	// handle the timeout first
	if (sendstat_connect_timeout < current_time)
	{
		sendstat_logf
			("sendstat_process_connect(): pending connection timed out.");
		sendstat_stage = SENDSTATSTAGE_ABORTED;
		return;
	}

	sendstat_logf("processing connection %s:80", inet_ntoa(sendstat_address));
#if defined(WIN32)
	{
		// use a select call to see if the socket to be ready for writing
		// set our select_timeout, to 0 seconds
		struct timeval select_timeout;
		fd_set fdWrite;			// connect success is reported here
		fd_set fdExcept;		// connect failure is reported here
		select_timeout.tv_sec = 0;
		select_timeout.tv_usec = 0;

		// prepare our file descriptor set
		FD_ZERO(&fdWrite);
		FD_SET(sendstat_socket, &fdWrite);
		FD_ZERO(&fdExcept);
		FD_SET(sendstat_socket, &fdExcept);

		// check if the socket is ready
		nRet =
			select(sendstat_socket + 1, NULL, &fdWrite, &fdExcept,
				   &select_timeout);

		sendstat_logf("select() returned %d", nRet);
		if (nRet < 1)
		{
			if (nRet == 0)
			{
				// it isn't ready yet - wait longer
			}
			else
			{
				sendstat_logf
					("sendstat_process_connect(): select() returned error %d",
					 nRet);
				sendstat_stage = SENDSTATSTAGE_ABORTED;
			}
			return;
		}

		if (FD_ISSET(sendstat_socket, &fdWrite))
		{
			// socket is ready for writing
			sendstat_logf
				("sendstat_process_connect(): connection established.");
			sendstat_stage = SENDSTATSTAGE_GENERATE_STATS;
			return;
		}

		if (FD_ISSET(sendstat_socket, &fdExcept))
		{
			// connection failed
			sendstat_logf("sendstat_process_connect(): connection failed.");
		}
		else
		{
			sendstat_logf
				("sendstat_process_connect(): don't know how we got here!");
		}
		sendstat_stage = SENDSTATSTAGE_ABORTED;
		return;
	}
#else
	// just attempt to connect the socket again using connect(), 
	// if the socket is now connected, connect() will return 0
	nRet =
		connect(sendstat_socket, (struct sockaddr *) &sockaddress,
				sizeof(struct sockaddr_in));
	if (nRet == 0)
	{
		// successful connection, generate the stats to post
		sendstat_stage = SENDSTATSTAGE_GENERATE_STATS;
		return;
	}
	if (nRet < 0)
	{
		if (errno != EINPROGRESS && errno != EALREADY)
		{
			sendstat_logf("sendstat_process_connect(): connect() error %d",
						  errno);
			sendstat_stage = SENDSTATSTAGE_ABORTED;
			return;
		}
	}
#endif
	return;
}

void sendstat_generate_stats()
{
	sendstat_stattext_to_post = sendstat_generate_statistics_text();
	sendstat_stage = SENDSTATSTAGE_POSTING;
}

void sendstat_post()
{
	char *msg = sendstat_stattext_to_post;

	int written;
	int msglen = strlen(msg);

	written = write(sendstat_socket, msg, msglen);

	if (written < 0)
	{							// check for an error
		sendstat_logf("sendstat_post(): An error occured posting statistics.");
		sendstat_stage = SENDSTATSTAGE_ABORTED;
		return;
	}

	// check for an incomplete write
	if (written < msglen)
	{
		sendstat_logf("Incomplete write, sent %d bytes of %d, write rest later",
					  written, msglen);
		sendstat_stattext_to_post += written;
		return;
	}

	// completed write
	sendstat_logf("Submitted %d bytes", written);

	sendstat_stage = SENDSTATSTAGE_CLOSE_int;
}

void sendstat_update()
{
	static time_t wait_until = 0;
	// this function is called every 10 seconds by default (PULSE_SENDSTAT)
	// it is necessary to be called this often due to is non blocking io    
	if ((int) sendstat_stage < (int) SENDSTATSTAGE_COMPLETED)
	{
		if (!wait_until || sendstat_stage != SENDSTATSTAGE_WAIT)
		{
			sendstat_logf("sendstat_update(%d)", (int) sendstat_stage);
		}
	}

	switch (sendstat_stage)
	{
	case SENDSTATSTAGE_WAIT:
		{
			if (wait_until == 0)
			{
				wait_until = current_time + MINUTE * 30;
			}
			else if (wait_until < current_time)
			{
				// time to move on
				wait_until = 0;
				sendstat_logf("moving on to resolving stage.");
				sendstat_resolve_domain();
			}
		}
		break;

	case SENDSTATSTAGE_DOMAIN_RESOLVED:
		sendstat_logf("initiating connection to '%s:80'",
					  inet_ntoa(sendstat_address));
		sendstat_initiate_connection();
		break;

	case SENDSTATSTAGE_CONNECT_IN_PROGRESS:
		sendstat_logf("processing connection.");
		sendstat_process_connect();
		break;

	case SENDSTATSTAGE_GENERATE_STATS:
		sendstat_logf("generating statistics.");
		sendstat_generate_stats();
		break;

	case SENDSTATSTAGE_POSTING:
		sendstat_logf("posting statistics.");
		sendstat_post();
		break;

	case SENDSTATSTAGE_CLOSE_int:
		sendstat_logf("closing socket.");
		close(sendstat_socket);
		sendstat_stage = SENDSTATSTAGE_COMPLETED;
		break;

	case SENDSTATSTAGE_COMPLETED:
	case SENDSTATSTAGE_ABORTED:
		// redo sendstats once per day
		{
			static time_t redo_in = 0;
			if (redo_in)
			{
				if (redo_in < current_time)
				{
					sendstat_logf("restarting sendstats");

					sendstat_stage = SENDSTATSTAGE_WAIT;
					redo_in = 0;
				}
			}
			else
			{
				redo_in = current_time + (24 * HOUR);	// every 24 hours
			}

		}
	default:
		break;
	};

}