/* * Module skeleton, by Carsten V. Munk 2001 * May be used, modified, or changed by anyone, no license applies. * You may relicense this, to any license */ /* for compile, use: EXLIBS="-lmysqlclient" make */ #define USE_MYSQL #ifdef USE_MYSQL #define MYCONF "wwwstats" #define DEFAULT_MYSQL_INTERVAL 900 #define list_add list_add_MYSQL #include #undef list_add #endif #include "unrealircd.h" #include "threads.h" #include #include struct chanStats_s { aChannel *chan; char chname[2*CHANNELLEN+1]; int msg; int exists; struct chanStats_s *next; }; struct channelInfo_s { int hashnum; aChannel *chan; int messages; }; struct asendInfo_s { int sock; char *buf; int bufsize; char *tmpbuf; }; typedef struct chanStats_s chanStats; typedef struct channelInfo_s channelInfo; typedef struct asendInfo_s asendInfo; int counter; time_t init_time; int stats_socket; THREAD thr; MUTEX chans_mutex; int chans_mutex_ai; char send_buf[4096]; struct sockaddr_un stats_addr; #ifdef USE_MYSQL THREAD mysql_thr; MYSQL *stats_db; #endif char* wwwstats_msg(aClient *sptr, aChannel *chptr, char *msg, int notice); void wwwstats_thr(void*); void asend_sprintf(asendInfo *info, char *fmt, ...); void append_int_param(asendInfo *info, char *param, int value); int getChannelInfo(channelInfo *prev); aChannel *getChanByName(char *name); void removeExpiredChannels(); char *tmp_escape(char *d, const char *a); void appendChannel(aChannel *ch, int messages); #ifdef USE_MYSQL void saveChannels(time_t act_time); void saveStats(time_t act_time); int mysql_query_sprintf(char *buf, char *fmt, ...); void wwwstats_mysql_thr(void *d); void loadChannels(void); void send_mysql_error(void); #endif int wwwstats_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs); int wwwstats_configposttest(int *errs); int wwwstats_configrun(ConfigFile *cf, ConfigEntry *ce, int type); chanStats *chans, *chans_last; // config file stuff, based on Gottem's module #ifdef USE_MYSQL static char *mysql_user; static char *mysql_pass; static char *mysql_db; static char *mysql_host; static int use_mysql; static int mysql_interval; #endif static char *socket_path; int socket_hpath=0; int wwwstats_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs) { ConfigEntry *cep; // For looping through our bl0cc int errors = 0; // Error count int i; // iter8or m8 #ifdef USE_MYSQL int mysql_huser=0, mysql_hpass=0, mysql_hdb=0, mysql_en=0, mysql_hhost=0; #endif // Since we'll add a new top-level block to unrealircd.conf, need to filter on CONFIG_MAIN lmao if(type != CONFIG_MAIN) return 0; // Returning 0 means idgaf bout dis // Check for valid config entries first if(!ce || !ce->ce_varname) return 0; // If it isn't our bl0ck, idc if(strcmp(ce->ce_varname, MYCONF)) return 0; // Loop dat shyte fam for(cep = ce->ce_entries; cep; cep = cep->ce_next) { // Do we even have a valid name l0l? if(!cep->ce_varname) { config_error("%s:%i: blank %s item", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF); // Rep0t error errors++; // Increment err0r count fam continue; // Next iteration imo tbh } #ifdef USE_MYSQL if(!strcmp(cep->ce_varname, "mysql-user")) { if(!cep->ce_vardata) { config_error("%s:%i: %s::%s must be a string", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname); errors++; // Increment err0r count fam continue; } mysql_huser=1; continue; } if(!strcmp(cep->ce_varname, "mysql-pass")) { if(!cep->ce_vardata) { config_error("%s:%i: %s::%s must be a string", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname); errors++; // Increment err0r count fam continue; } mysql_hpass=1; continue; } if(!strcmp(cep->ce_varname, "mysql-db")) { if(!cep->ce_vardata) { config_error("%s:%i: %s::%s must be a string", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname); errors++; // Increment err0r count fam continue; } mysql_hdb=1; continue; } if(!strcmp(cep->ce_varname, "mysql-host")) { if(!cep->ce_vardata) { config_error("%s:%i: %s::%s must be a string", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname); errors++; // Increment err0r count fam continue; } mysql_hhost=1; continue; } if(!strcmp(cep->ce_varname, "mysql-interval")) { if(!cep->ce_vardata) { config_error("%s:%i: %s::%s must be an integer between 1 and 1000 (minutes)", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname); errors++; // Increment err0r count fam continue; // Next iteration imo tbh } // Should be an integer yo for(i = 0; cep->ce_vardata[i]; i++) { if(!isdigit(cep->ce_vardata[i])) { config_error("%s:%i: %s::%s must be an integer between 1 and 1000 (minutes)", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname); errors++; // Increment err0r count fam break; } } if(!errors && (atoi(cep->ce_vardata) < 1 || atoi(cep->ce_vardata) > 1000)) { config_error("%s:%i: %s::%s must be an integer between 1 and 1000 (minutes)", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname); errors++; // Increment err0r count fam } continue; } if(!strcmp(cep->ce_varname, "use-mysql")) { // no value expected mysql_en = 1; continue; } #endif if(!strcmp(cep->ce_varname, "socket-path")) { if(!cep->ce_vardata) { config_error("%s:%i: %s::%s must be a path", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname); errors++; // Increment err0r count fam continue; } socket_hpath = 1; continue; } // Anything else is unknown to us =] config_warn("%s:%i: unknown item %s::%s", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, MYCONF, cep->ce_varname); // So display just a warning } if(mysql_en && (!mysql_huser || !mysql_hpass || !mysql_hdb || !mysql_hhost)){ config_warn("m_wwwstats: error: your mysql configuration is incomplete! Please either correct or disable it!"); errors++; } *errs = errors; return errors ? -1 : 1; // Returning 1 means "all good", -1 means we shat our panties } int wwwstats_configposttest(int *errs) { if(!socket_hpath){ config_warn("m_wwwstats: warning: socket path not specified! Socket won't be created."); } return 1; } // "Run" the config (everything should be valid at this point) int wwwstats_configrun(ConfigFile *cf, ConfigEntry *ce, int type) { ConfigEntry *cep; // For looping through our bl0cc // Since we'll add a new top-level block to unrealircd.conf, need to filter on CONFIG_MAIN lmao if(type != CONFIG_MAIN) return 0; // Returning 0 means idgaf bout dis // Check for valid config entries first if(!ce || !ce->ce_varname) return 0; // If it isn't our bl0cc, idc if(strcmp(ce->ce_varname, MYCONF)) return 0; // Loop dat shyte fam for(cep = ce->ce_entries; cep; cep = cep->ce_next) { // Do we even have a valid name l0l? if(!cep->ce_varname) continue; // Next iteration imo tbh #ifdef USE_MYSQL if(cep->ce_vardata && !strcmp(cep->ce_varname, "mysql-user")) { mysql_user = strdup(cep->ce_vardata); continue; } if(cep->ce_vardata && !strcmp(cep->ce_varname, "mysql-pass")) { mysql_pass = strdup(cep->ce_vardata); continue; } if(cep->ce_vardata && !strcmp(cep->ce_varname, "mysql-db")) { mysql_db = strdup(cep->ce_vardata); continue; } if(cep->ce_vardata && !strcmp(cep->ce_varname, "mysql-host")) { mysql_host = strdup(cep->ce_vardata); continue; } if(!strcmp(cep->ce_varname, "mysql-interval")) { mysql_interval = atoi(cep->ce_vardata); continue; } if(!strcmp(cep->ce_varname, "use-mysql")) { use_mysql = 1; continue; } #endif if(cep->ce_vardata && !strcmp(cep->ce_varname, "socket-path")) { socket_path = strdup(cep->ce_vardata); continue; } } #ifdef USE_MYSQL if(mysql_interval == 0) mysql_interval = DEFAULT_MYSQL_INTERVAL; #endif return 1; // We good } ModuleHeader MOD_HEADER(m_wwwstats) = { "m_wwwstats", /* Name of module */ "$Id: v1.07 2018/12/28 rocket/k4be$", /* Version */ "Provides data for network stats", /* Short description of module */ "3.2-b8-1", NULL }; // Configuration testing-related hewks go in testing phase obv MOD_TEST(m_wwwstats) { // We have our own config block so we need to checkem config obv m9 // Priorities don't really matter here HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, wwwstats_configtest); HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, wwwstats_configposttest); return MOD_SUCCESS; } /* This is called on module init, before Server Ready */ MOD_INIT(m_wwwstats) { /* * We call our add_Command crap here */ HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, wwwstats_configrun); HookAddPChar(modinfo->handle, HOOKTYPE_PRE_CHANMSG, 0, wwwstats_msg); return MOD_SUCCESS; } /* Is first run when server is 100% ready */ MOD_LOAD(m_wwwstats) { #ifdef USE_MYSQL MYSQL_RES *res; MYSQL_ROW row; #endif if(socket_path){ stats_addr.sun_family = AF_UNIX; strcpy(stats_addr.sun_path, socket_path); unlink(stats_addr.sun_path); } #ifdef USE_MYSQL stats_db = mysql_init(NULL); if(!stats_db) send_mysql_error(); #endif IRCCreateMutex(chans_mutex); chans_mutex_ai = 0; counter = 0; chans = NULL; chans_last = NULL; if(socket_path){ stats_socket = socket(PF_UNIX, SOCK_STREAM, 0); bind(stats_socket, (struct sockaddr*) &stats_addr, SUN_LEN(&stats_addr)); chmod(socket_path, 0777); listen(stats_socket, 5); } #ifdef USE_MYSQL if(use_mysql && mysql_host && mysql_user && mysql_pass && mysql_db){ mysql_real_connect(stats_db, mysql_host, mysql_user, mysql_pass, mysql_db, 0, NULL, 0); mysql_query(stats_db, "CREATE TABLE IF NOT EXISTS `chanlist` (`id` int(11) NOT NULL AUTO_INCREMENT, `date` int(11), `name` char(64), `topic` text, `users` int(11), `messages` int(11), PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`,`users`,`messages`), KEY `name_3` (`name`), KEY `date` (`date`) )"); mysql_query(stats_db, "CREATE TABLE IF NOT EXISTS `stat` (`id` int(11) NOT NULL AUTO_INCREMENT, `date` int(11), `clients` int(11), `servers` int(11), `messages` int(11), `channels` int(11), PRIMARY KEY (`id`), UNIQUE KEY `changes` (`clients`,`servers`,`messages`,`channels`), KEY `date` (`date`) )"); mysql_query(stats_db, "SELECT messages FROM stat ORDER BY id DESC LIMIT 1"); res = mysql_use_result(stats_db); if(!res) send_mysql_error(); else { if((row = mysql_fetch_row(res))) { counter = strtoul(row[0], NULL, 10); } mysql_free_result(res); } loadChannels(); } #endif IRCCreateThread(thr, wwwstats_thr, NULL); #ifdef USE_MYSQL if(stats_db) IRCCreateThread(mysql_thr, wwwstats_mysql_thr, NULL); #endif return MOD_SUCCESS; } /* Called when module is unloaded */ MOD_UNLOAD(m_wwwstats) { time_t act_time; chanStats *next; pthread_cancel(thr); pthread_join(thr, NULL); #ifdef USE_MYSQL if(stats_db){ pthread_cancel(mysql_thr); pthread_join(mysql_thr, NULL); } #endif close(stats_socket); unlink(stats_addr.sun_path); act_time = time(NULL); #ifdef USE_MYSQL saveStats(act_time); saveChannels(act_time); #endif // for(;chans;chans=chans->next) free(chans); for(;chans;chans=next) { next=chans->next; free(chans); } #ifdef USE_MYSQL if(stats_db) mysql_close(stats_db); if(mysql_user) free(mysql_user); if(mysql_pass) free(mysql_pass); if(mysql_db) free(mysql_db); if(mysql_host) free(mysql_host); #endif if(socket_path) free(socket_path); return MOD_SUCCESS; } char* wwwstats_msg(aClient *sptr, aChannel *chptr, char *msg, int notice) { chanStats *lp; #ifdef USE_MYSQL char name[2*CHANNELLEN+1]; char buf[2048]; MYSQL_RES *res; MYSQL_ROW row; #endif int c_msg; counter++; for(lp=chans; lp; lp=lp->next) if(lp->chan==chptr) break; if(lp) lp->msg++; else { c_msg = 1; #ifdef USE_MYSQL if(use_mysql){ tmp_escape(name, chptr->chname); mysql_query_sprintf(buf, "SELECT MAX(messages) FROM chanlist WHERE name='%s' GROUP BY name", name); res = mysql_use_result(stats_db); if(!res) send_mysql_error(); else { if((row = mysql_fetch_row(res))) if(row[0]) c_msg = strtoul(row[0], NULL, 10); mysql_free_result(res); } } #endif // sendto_realops("wwwstats: added channel %s, %d msgs", name, c_msg); appendChannel(chptr, c_msg); } return msg; } void wwwstats_thr(void *d) { char buf[2000]; char topic[2*TOPICLEN+1]; char name[2*CHANNELLEN+1]; int i; int sock; channelInfo chinfo; asendInfo asinfo; struct sockaddr_un cli_addr; socklen_t slen = sizeof(cli_addr); asinfo.buf = send_buf; asinfo.bufsize = sizeof(send_buf); asinfo.tmpbuf = buf; aClient *acptr; while(1) { sock = accept(stats_socket, (struct sockaddr*) &cli_addr, &slen); if(sock<0) break; asinfo.sock = sock; send_buf[0] = 0; append_int_param(&asinfo, "clients", IRCstats.clients); append_int_param(&asinfo, "channels", IRCstats.channels); append_int_param(&asinfo, "operators", IRCstats.operators); append_int_param(&asinfo, "servers", IRCstats.servers); append_int_param(&asinfo, "messages", counter); i=0; list_for_each_entry(acptr, &global_server_list, client_node){ if (IsULine(acptr) && HIDE_ULINES) continue; asend_sprintf(&asinfo, "$stats['serv'][%d]['name'] = '%s';\n", i, acptr->name); asend_sprintf(&asinfo, "$stats['serv'][%d]['users'] = %ld;\n", i, acptr->serv->users); i++; } IRCMutexLock(chans_mutex); if(!chans_mutex_ai) removeExpiredChannels(); chans_mutex_ai++; IRCMutexUnlock(chans_mutex); chinfo.chan = NULL; i=0; while(getChannelInfo(&chinfo)) { if(!PubChannel(chinfo.chan)) continue; asend_sprintf(&asinfo, "$stats['chan'][%d]['name'] = '%s';\n", i, tmp_escape(name, chinfo.chan->chname)); asend_sprintf(&asinfo, "$stats['chan'][%d]['users'] = %d;\n", i, chinfo.chan->users); asend_sprintf(&asinfo, "$stats['chan'][%d]['messages'] = %d;\n", i, chinfo.messages); if(chinfo.chan->topic) asend_sprintf(&asinfo, "$stats['chan'][%d]['topic'] = '%s';\n", i, tmp_escape(topic, chinfo.chan->topic)); i++; } IRCMutexLock(chans_mutex); chans_mutex_ai--; IRCMutexUnlock(chans_mutex); if(send_buf[0]) { send(sock, send_buf, strlen(send_buf), 0); send_buf[0] = 0; } close(sock); } } #ifdef USE_MYSQL void wwwstats_mysql_thr(void *d) { time_t prev_time; time_t act_time; prev_time = 0; while(1) { act_time = time(NULL); if((act_time-prev_time)>=mysql_interval) { saveStats(act_time); IRCMutexLock(chans_mutex); if(!chans_mutex_ai) removeExpiredChannels(); chans_mutex_ai++; IRCMutexUnlock(chans_mutex); saveChannels(act_time); IRCMutexLock(chans_mutex); chans_mutex_ai--; IRCMutexUnlock(chans_mutex); prev_time = act_time; } sleep(10); } } void saveChannels(time_t act_time) { char buf[2*(TOPICLEN+CHANNELLEN)+256]; char name[2*CHANNELLEN+1]; char topic[2*TOPICLEN+1]; channelInfo chinfo; if(!use_mysql) return; chinfo.chan = NULL; while(getChannelInfo(&chinfo)) { tmp_escape(name, chinfo.chan->chname); if(chinfo.chan->topic) tmp_escape(topic, chinfo.chan->topic); else topic[0] = 0; mysql_query_sprintf(buf, "INSERT IGNORE INTO chanlist VALUES (NULL, %d, '%s', '%s', %d, %d)", act_time, name, topic, chinfo.chan->users, chinfo.messages); } } void loadChannels(void){ // channel will be added automatically when a message comes char buf[2*CHANNELLEN+20]; char name[2*CHANNELLEN+1]; aChannel *ch; MYSQL_RES *res; MYSQL_ROW row; channelInfo chinfo; unsigned long cnt; if(!use_mysql) return; chinfo.chan = NULL; while(getChannelInfo(&chinfo)) { ch = chinfo.chan; tmp_escape(name, chinfo.chan->chname); mysql_query_sprintf(buf, "SELECT MAX(messages) FROM chanlist WHERE name = '%s'", name); res = mysql_use_result(stats_db); if(res && (row = mysql_fetch_row(res))){ cnt = 0; if(row[0]) cnt = strtoul(row[0], NULL, 10); if(cnt > 0){ appendChannel(ch, cnt); // sendto_realops("wwwstats: added from db: %s, %lu msgs", ch->chname, cnt); } mysql_free_result(res); } } } void saveStats(time_t act_time) { char buf[512]; if(!use_mysql) return; mysql_query_sprintf(buf, "INSERT IGNORE INTO stat VALUES (NULL, %d, %d, %d, %d, %d)", act_time, IRCstats.clients, IRCstats.servers, counter, IRCstats.channels); } void send_mysql_error(void){ sendto_realops("wwwstats: mysql error: %s",mysql_error(stats_db)); } #endif void appendChannel(aChannel *ch, int messages) { chanStats *lp; lp = malloc(sizeof(chanStats)); lp->chan = ch; lp->msg = messages; strcpy(lp->chname, ch->chname); lp->next = NULL; if(chans_last) chans_last->next = lp; chans_last = lp; if(!chans) chans = lp; } void removeExpiredChannels() { int hashnum; aChannel *c; chanStats *lp, *lpprev, *lpnext; for(lp=chans; lp; lp=lp->next) lp->exists = 0; for(hashnum=0; hashnumnext) if(lp->chan==c) break; if(lp) lp->exists = 1; c = c->hnextch; } } lpprev = NULL; lpnext = NULL; for(lp=chans; lp; lp=lpnext) { if(!lp->exists) { // sendto_realops("wwwstats: deleted channel %s", lp->chname); if(lpprev) lpprev->next = lp->next; else chans = lp->next; if(!lp->next) chans_last = lpprev; lpnext = lp->next; free(lp); continue; } lpnext = lp->next; lpprev = lp; } } aChannel *getChanByName(char *name) { channelInfo chinfo; chinfo.chan = NULL; while(getChannelInfo(&chinfo)) { if(strcmp(chinfo.chan->chname, name)==0) return chinfo.chan; } return NULL; } int getChannelInfo(channelInfo *prev) { int hashnum = 0; int messages = 0; aChannel *c = NULL; chanStats *lp; if(prev->chan) { hashnum = prev->hashnum; c = prev->chan->hnextch; if(!c) hashnum++; } if(!c) for(; hashnumnext) if(lp->chan==c) break; if(lp) messages = lp->msg; prev->hashnum = hashnum; prev->chan = c; prev->messages = messages; return 1; } #ifdef USE_MYSQL int mysql_query_sprintf(char *buf, char *fmt, ...) { int ret; va_list list; va_start(list, fmt); vsprintf(buf, fmt, list); va_end(list); ret = mysql_query(stats_db, buf); if(ret){ sendto_realops("wwwstats: mysql query error: %s",mysql_error(stats_db)); } return ret; } #endif void asend_sprintf(asendInfo *info, char *fmt, ...) { int bl, tl; va_list list; va_start(list, fmt); vsprintf(info->tmpbuf, fmt, list); bl = strlen(info->tmpbuf); tl = strlen(info->buf); if((bl+tl)>=info->bufsize) { send(info->sock, info->buf, tl, 0); info->buf[0] = 0; } strcat(info->buf, info->tmpbuf); va_end(list); } void append_int_param(asendInfo *info, char *param, int value) { asend_sprintf(info, "$stats['%s'] = %d;\n", param, value); } char *tmp_escape(char *d, const char *a) { int diff; int i; diff = 0; for(i=0; a[i]; i++) { if((a[i]=='\'') || (a[i]=='\\')) { d[diff+i] = '\\'; diff++; } d[diff+i] = a[i]; } d[diff+i] = 0; return d; }