/**************************************************************************/
// 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;
};
}