/*
* Module skeleton, by Carsten V. Munk 2001 <stskeeps@tspre.org>
* 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 <mysql/mysql.h>
#undef list_add
#endif
#include "unrealircd.h"
#include "threads.h"
#include <sys/socket.h>
#include <sys/un.h>
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++) {
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);
#ifdef USE_MYSQL
saveStats(act_time);
saveChannels(act_time);
#endif
// for(;chans;chans=chans->next) free(chans);
for(;chans;chans=next) {
next=chans->next;
}
#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) {
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; hashnum<CH_MAX; hashnum++) {
c = (aChannel*) hash_get_chan_bucket(hashnum);
while(c) {
for(lp=chans; lp; lp=lp->next) 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;
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(; hashnum<CH_MAX; hashnum++) {
c = (aChannel*) hash_get_chan_bucket(hashnum);
if(c) break;
}
if(!c) return 0;
for(lp=chans; lp; lp=lp->next) 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;
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;
if((bl+tl)>=info->bufsize) {
send(info->sock, info->buf, tl, 0);
info->buf[0] = 0;
}
strcat(info
->buf
, info
->tmpbuf
);
}
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;
}