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