/*********************************************************************************** * Author: Dmitry Isaenko * * License: GNU GPL v.3 * * Version: 1.4 * * Site: https://developersu.blogspot.com/ * * 2017, Russia * ***********************************************************************************/ #include "libircclient/libircclient.h" #include "libircclient/libirc_rfcnumeric.h" #include "stdio.h" #include "string.h" #include #include #include #include // only to get PATH_MAX #include // for using setsid. #include // Use syslog #include // Use signals #include // Use pselect #include "defined_values.h" #include "daemon.c" // TODO make it multi-channel typedef struct conf_sruct { int status; char server[2048]; char channel[2048]; int port; char nick[2048]; char username[2048]; char realname[2048]; char password[2048]; int maxNickLength; char logPath[PATH_MAX]; char link[2048]; // should be enough to store link int reJoin; // 1 - yes, 0 - no int floodTimeOut; } configuration; configuration load_config(int run, char * nick_or_path) { /* * if run = 0 we load config from the file * if run = 1 we return configuration structure stored in this function * if run = 2 we overrite 'nick' at the configuration structure */ int i; FILE * conf; static char fileArray[15][2048]; // user setup stored here and avaliable by any call after initial (by passing 0 to function) char * st; char sta[15][2][2048]; // should be 3D static configuration C; //returning structure char confFilePath[PATH_MAX]; int checksum = 0; if (run == 0) { // the first time call if ( (conf = fopen("/etc/loperIRCLogBot/bot.conf","r")) != NULL){ printf("Using configuration file stored at /etc/loperIRCLogBot/bot.conf\n" "Prefere using the one, stored in this folder? Just rename or delete the one from /etc...\n"); for (i=0;(!feof(conf));i++){ fgets(fileArray[i], sizeof(fileArray[i]), conf); if (i>=14){ // 15 lines is max size for config file C.status = -2; return C; } } } else { strcpy(confFilePath, nick_or_path); strcat(confFilePath, "bot.conf"); // add bot.conf to path-to-executable printf("Using configuration file stored at %s\n" "Please note, configuration file also could be stored at '/etc/loperIRCLogBot/bot.conf' and have higher priority\n", confFilePath); if ( (conf = fopen(confFilePath,"r")) != NULL) { for (i=0;(!feof(conf));i++){ fgets(fileArray[i], sizeof(fileArray[i]), conf); if (i>=14){ // 15 lines is max size for config file C.status = -2; return C; } } } else { C.status = -2; //unable to open file or it's too big = -2 return C; } } fclose(conf); #ifdef DEBUG_LOG printf("___Content of the file___\n"); for (i=0;i<15; i++){ printf("%s", fileArray[i]); } printf("______________________\n"); #endif // Parsing configuration file - IF STRING DOESN'T HAVE ":" - SKIP IT for (i=0;i<15;i++){ if ( strstr(fileArray[i], ":") != NULL ) { st = strtok(fileArray[i], ": \t\n"); // if we can't get ANY parameter at the string, that have ":" inside, then skip this string if (st != NULL){ strcpy(sta[i][0],st); st = strtok(NULL, " \t\n"); // if we see anything before 'space' \t or \n if (st == NULL){ // if we had a first parameter in string, (no matter was it set before ":" or after) and don't have the second one, C.status = -1; // then we ruin the chain and returning error. 'Status' of incorrect config-file = "-1" return C; } else { strcpy(sta[i][1], st); if ( strstr(sta[i][0], "link") != NULL ){ // if it's 'link', then add here everything user wrote st = strtok(NULL, "\n"); while (st != NULL){ strcat(sta[i][1], st); st = strtok(NULL, "\n"); } } } } } } for (i=0;i<15;i++){ if ( strstr(sta[i][0], "server") != NULL ) checksum |= 1<<0; else if ( strstr(sta[i][0], "channel") != NULL ) checksum |= 1<<1; else if ( strstr(sta[i][0], "port") != NULL ) checksum |= 1<<2; else if ( strstr(sta[i][0], "nick") != NULL ) checksum |= 1<<3; else if ( strstr(sta[i][0], "username") != NULL ) checksum |= 1<<4; else if ( strstr(sta[i][0], "realname") != NULL ) checksum |= 1<<5; else if ( strstr(sta[i][0], "password") != NULL ) checksum |= 1<<6; else if ( strstr(sta[i][0], "maxNickLength") != NULL ) checksum |= 1<<7; else if ( strstr(sta[i][0], "logPath") != NULL ) checksum |= 1<<8; else if ( strstr(sta[i][0], "link") != NULL ) checksum |= 1<<9; else if ( strstr(sta[i][0], "reJoin") != NULL ) checksum |= 1<<10; else if ( strstr(sta[i][0], "floodTimeOut") != NULL ) checksum |= 1<<11; } if (checksum != 0b111111111111){ C.status = checksum; // incorrect number of settings defined return C; } else { // Format array for return in case we're all good C.status = 0; // OK = 0. for (i=0; i<15; i++){ if ((strcmp(sta[i][0], "server")) == 0) strcpy(C.server, sta[i][1]); else if ( (strcmp(sta[i][0], "port")) == 0) C.port = atoi(sta[i][1]); else if ( (strcmp(sta[i][0], "channel")) == 0) strcpy(C.channel, sta[i][1]); else if ( (strcmp(sta[i][0], "nick")) == 0) strcpy(C.nick, sta[i][1]); else if ( (strcmp(sta[i][0], "username")) == 0) strcpy(C.username, sta[i][1]); else if ( (strcmp(sta[i][0], "realname")) == 0) strcpy(C.realname, sta[i][1]); else if ( (strcmp(sta[i][0], "password")) == 0) strcpy(C.password, sta[i][1]); else if ( (strcmp(sta[i][0], "maxNickLength")) == 0){ C.maxNickLength = atoi(sta[i][1]); if (C.maxNickLength > 128){ C.maxNickLength = 128; // now idiots could feel themselfs protected. Libircclient restriction IIRC set to 128 chars } } else if ( (strcmp(sta[i][0], "logPath")) == 0){ if (strcmp(sta[i][1], "0") == 0) strcpy(C.logPath, nick_or_path); else { if (sta[i][1][0] != '/'){ C.status = -3; return C; } else{ strcpy(C.logPath, sta[i][1]); if ( C.logPath[strlen(C.logPath)] != '/' ) strcat (C.logPath, "/"); } } } else if ( (strcmp(sta[i][0], "link")) == 0){ if (strcmp(sta[i][1], "0") == 0) strcpy(C.link, ";)"); else strcpy(C.link, sta[i][1]); } else if ( (strcmp(sta[i][0], "reJoin")) == 0){ if (strcmp(sta[i][1], "yes") == 0 || strcmp(sta[i][1], "Yes") == 0 ) C.reJoin = 1; else C.reJoin = 0; } else if ( (strcmp(sta[i][0], "floodTimeOut")) == 0) C.floodTimeOut = atoi(sta[i][1]); } if (strlen(C.nick) > C.maxNickLength) C.nick[C.maxNickLength] = '\0'; // yeah, they will love it, before set nick name longer then 128 char =( } // ++++++++++++++++++++++++++++++++++++++++++++++ #ifdef DEBUG_LOG printf("___Recieved keys from the file___\n"); for (i=0;i<15; i++){ printf("%s - %s\n", sta[i][0], sta[i][1]); } printf("______________________\n"); #endif return C; } else if ( run == 1 ){ return C; // just return already loaded structure by request } else if ( run == 2){ // save nick recieved strcpy(C.nick, nick_or_path); return C; } } int createDir(char * logdir, char * newDirName){ struct stat st = {0}; char dir[PATH_MAX]; strcpy(dir, logdir); strcat(dir, newDirName); // add bot.conf to path-to-executable if (stat(dir, &st) == -1) { if (mkdir(dir, 0777) != 0 ) return 1; } return 0; } // Function to print time stamps when writing to console const char * printTimeStamp(){ // const function, static array.. one say this function should be replaced to something better. It's make me sad. static char timeStamp[22]; time_t now = time(NULL); strftime(timeStamp, 22, "%d %b %Y %X", localtime(&now)); return timeStamp; } // Mainly used to update is_alive void event_ctcp_rep(irc_session_t *session, const char *event, const char *origin, const char **params, unsigned int count){ // handle own ctcp requests - get recieved data and do something with it configuration C = load_config(1, ""); //load config to get current nickname #ifdef PING_DEBUG printf("\nReply from my CTCP, where USER = %s\n", origin); #endif if (strncmp(origin, C.nick, strlen(C.nick)) == 0){ // if bot generated response, then it's ping for internal use. (Ok, documentation is 80% clear on this, it will work but in future should be checked) is_alive(1, time(NULL), session); } } /* Handle timeouts before re-connect (rewrite in January 2038) state = 0 - get status state = 1 - write status returns 0 - OK returns 1 - not OK */ int is_alive( int state, time_t timeGot, irc_session_t * session){ // in future, it should be stand-alone thread static time_t timeSav; static int pingRequestSent = 0; // 0 - not sent, 1 - already sent configuration C = load_config(1, ""); //load config to get current nickname #ifdef PING_DEBUG printf("is_alive:ping = %d\n", pingRequestSent); #endif if (state == 0) { if ( ( timeGot - timeSav) > TIME_TO_SEND_PING ) { // ok, it's time to send PING to yourself or probably we have problems if (pingRequestSent == 0){ // if we didn't sent PING request before, let's do it and change pingRequestSent #ifdef PING_DEBUG printf("PANIC - Request sent\n"); printf(" stored time = %ld\n",timeSav); printf(" recieved time = %ld\n",timeGot); #endif irc_cmd_ctcp_request(session, C.nick, "PING"); pingRequestSent = 1; return 0; } else{ // we already sent PING if (( timeGot - timeSav) > (TIME_TO_SEND_PING+TIME_TO_WAIT_PING)){ // we know, that the time is greater than stored one, so we add 3min(180) and if it's greater, then re-connect #ifdef PING_DEBUG printf("Now we have problems\n"); #endif return 1; } else return 0; } } } else if ( state == 1 ){ timeSav = timeGot; // re-write stored time pingRequestSent = 0; // reset pingRequestSent #ifdef PING_DEBUG printf("\n1-WRITE event\nTime Saved (re-written) = %ld\n", timeSav); //debug printf("TIME_TO_SEND_PING = %d\n", TIME_TO_SEND_PING); //debug #endif return 0; } return 0; // how it could be possible to come here? ok, whatever } void event_connect (irc_session_t * session, const char * event, const char * origin, const char ** params, unsigned int count) { is_alive(1, time(NULL), session ); // this time really 'initial' timestamp written into the watchdog function configuration C = load_config(1, ""); // dump_event (session, event, origin, params, count); #ifdef X_MODE irc_cmd_user_mode (session, "+x"); // TODO clarify this shit #endif irc_cmd_join (session, C.channel, 0); } void dump_event (irc_session_t * session, const char * event, const char * origin, const char ** params, unsigned int count) { // TODO: Re-write. Actually, this implementation is also pice of shit #ifdef DEBUG // It cause too many system calls even we don't need them char buf[512]; int cnt; buf[0] = '\0'; for ( cnt = 0; cnt < count; cnt++ ) { if ( cnt ) strcat (buf, "|"); strcat (buf, params[cnt]); } #endif static time_t floodTrackTime = 0; char nowTime[20]; char nickBuf[128]; char hostBuf[1024]; time_t now = time(NULL); FILE * fp; // set FD for the file for logs configuration C = load_config(1, ""); // Set name for the log file irc_target_get_nick (origin, nickBuf, 128); irc_target_get_host (origin, hostBuf, 1024); strftime(nowTime, 20, "logs/%Y-%m-%d.txt", localtime(&now)); strcat(C.logPath, nowTime); // now C.logPath have the correct name of the file if ( (fp = fopen (C.logPath, "ab")) != NULL ) { strftime(nowTime,20, "[%H:%M:%S] ", localtime(&now)); !(strcmp(event,"CHANNEL")) ? fprintf(fp,"%s <%s>: %s\n",nowTime, nickBuf, params[1]): !(strcmp(event,"ACTION")) ? fprintf(fp,"%s %s %s\n",nowTime, nickBuf, params[1]): !(strcmp(event,"JOIN")) ? fprintf(fp,"%s >> %s [%s] joined %s\n",nowTime, nickBuf, hostBuf, params[0]): !(strcmp(event,"INVITE")) ? fprintf(fp,"%s %s invites %s to %s\n",nowTime, nickBuf, params[0], params[1]): !(strcmp(event,"KICK")) ? fprintf(fp,"%s !<< %s kicked by %s [%s] with reason: %s\n",nowTime, params[1], nickBuf, hostBuf, params[2]): !(strcmp(event,"MODE")) ? strcmp(nickBuf, hostBuf) == 0 ? 0: fprintf(fp,"%s -!- %s [%s] set %s %s\n",nowTime, nickBuf, hostBuf, params[1], params[2] ? params[2]:""): !(strcmp(event,"PART")) ? fprintf(fp,"%s << %s [%s] parted: %s \n",nowTime, nickBuf, hostBuf, params[1]): !(strcmp(event,"TOPIC")) ? fprintf(fp,"%s -!- %s [%s] has changed topic to: %s \n",nowTime, nickBuf, hostBuf, params[1]): !(strcmp(event,"QUIT")) ? fprintf(fp,"%s << %s [%s] quit: %s \n",nowTime, nickBuf, hostBuf, params[0]): !(strcmp(event,"NICK")) ? fprintf(fp,"%s -!- %s [%s] changed nick to: %s \n",nowTime, nickBuf, hostBuf, params[0]): 0; if (!strcmp(event,"KICK")){ // Reloading configuration in case our nick has been changed recently if ( !(strcmp(params[1], C.nick)) && C.reJoin == 1 ){ event_connect (session, event, origin, params, count); } } else if (!strcmp(event, "CHANNEL")) // works like shit. Should be separated thread if (strncmp(params[1], C.nick, strlen(C.nick)) == 0 ) if ((now - floodTrackTime) >= C.floodTimeOut){ irc_cmd_msg(session, params[0], C.link); floodTrackTime = now; fprintf(fp,"%s <%s>: %s\n",nowTime, C.nick, C.link); } fclose (fp); } else { printf("Unable to open/create log file: check folder permissions\n"); // and die, but it doesn't die.. well. Ok for now. exit(EXIT_FAILURE); // TODO: QA: integration testing needed } #if defined (DEBUG) printf ("Event \"%s\", origin: \"%s\", params: %d [%s]\n", event, origin ? origin : "NULL", cnt, buf); #endif } void event_ctcp_req(irc_session_t * session, const char * event, const char * origin, const char ** params, unsigned int count) { dump_event (session, event, origin, params, count); time_t now = time(NULL); char curTime[138]; strftime(curTime, 138, "TIME %a %b %d %H:%M:%S %Z %Y", localtime(&now)); if( strcmp (params[0], "VERSION") == 0 ){ dump_event (session, event, origin, params, count); irc_cmd_ctcp_reply(session, origin, "VERSION loperIRCLogBot v."__CUR_VER__); } else if( strcmp (params[0], "SOURCE") == 0 ){ dump_event (session, event, origin, params, count); irc_cmd_ctcp_reply(session, origin, "SOURCE loperIRCLogBot v."__CUR_VER__); } else if( strcmp (params[0], "TIME") == 0 ){ dump_event (session, event, origin, params, count); irc_cmd_ctcp_reply(session, origin, curTime); } else if( strncmp(params[0], "PING", 4) == 0 ){ dump_event (session, event, origin, params, count); irc_cmd_ctcp_reply (session, origin, params[0]); } else if( strcmp (params[0], "CLIENTINFO") == 0 ){ dump_event (session, event, origin, params, count); irc_cmd_ctcp_reply(session, origin, "CLIENTINFO loperIRCLogBot v."__CUR_VER__" - Supported tags: VERSION, SOURCE, TIME, PING, CLIENTINFO"); } } void event_join (irc_session_t * session, const char * event, const char * origin, const char ** params, unsigned int count) { dump_event (session, event, origin, params, count); irc_cmd_user_mode (session, "+i"); } void nick_change(irc_session_t * session){ static char append[] = "|0"; configuration C = load_config(1, ""); if ( append[1] > '9') // Refactoring request append[1] = '0'; // if all your nicknames with |0 |2 ... |9 are already occupied, we're fucked up. Really, there is no good solution in this code. // Let's say that we have string like 1234567|0, and maxNickLength = 10, then we see, that string length is greater (9) then maxNickLength-2, but also we've already appended something like "|0" into it, then // we could check it and wipe. Otherwise we wipe 2 last chars. if ( strlen(C.nick) > (C.maxNickLength - 2) ){ if ( (C.nick[C.maxNickLength-3] == '|') && (C.nick[C.maxNickLength-2] >= '0') && (C.nick[C.maxNickLength-2] <= '9') ) C.nick[C.maxNickLength-3] = '\0'; else C.nick[C.maxNickLength-2] = '\0'; } if ( C.nick[strlen(C.nick)-2] == '|' ) C.nick[strlen(C.nick)-1] = append[1]; else strncat(C.nick, append, 2); append[1]++; C = load_config(2, C.nick); irc_cmd_nick(session, C.nick); } void event_numeric (irc_session_t * session, unsigned int event, const char * origin, const char ** params, unsigned int count) { char buf[24]; sprintf (buf, "%d", event); if (event == 433){ // 433 Nickname is already in use nick_change(session); } dump_event (session, buf, origin, params, count); } static void event_notice (irc_session_t * session, const char * event, const char * origin, const char ** params, unsigned int count) { char nickBuf[1024]; // too big?? configuration C = load_config(1,""); dump_event (session, event, origin, params, count); if ( !origin ) return; else irc_target_get_nick (origin, nickBuf, 1024); if ( strcasecmp (nickBuf, "nickserv") ) return; if ( (strstr (params[1], "This nickname is registered") != NULL) || (strstr (params[1], "This nickname is owned by someone else") != NULL) ){ irc_send_raw(session, "nickserv IDENTIFY %s", C.password); } else if( strstr (params[1], "Password accepted") != NULL ) printf ("Nickserv authentication succeeded\n"); } int makeTemplate(char * folder){ FILE * templ; strcat (folder, "bot.conf"); printf("Attention! This action will remove your current bot.conf file\n" "Where do you want to generate template?:\n" "(1) This folder\n" "(2) /etc/loperIRCLogBot/\n" "(A) Abort\n"); do{ switch (getchar()){ case '1': if ( (templ = fopen(folder,"w")) != NULL) { fprintf(templ, DEF_DEFAULT_TEMPLATE); fclose(templ); printf("Configuratation template created.\n"); return 0; } else { printf("Unable to create configuration template file\n" "Check folder permissions\n"); return 1; } case '2': if (createDir("/etc/loperIRCLogBot/","") != 0){ printf("Unable to create diretory /etc/loperIRCLogBot\n" "Make sure that you have permissions to write there\n"); return 1; } if ( (templ = fopen("/etc/loperIRCLogBot/bot.conf","w")) != NULL) { fprintf(templ, DEF_DEFAULT_TEMPLATE); fclose(templ); printf("Configuratation template created.\n"); return 0; } else { printf("Unable to create configuration template file\n" "Check folder permissions\n"); return 1; } case 'A': case 'a': printf("No changes applied\n"); return 0; case '\n': // ,__o break; // _-\_<, default: // (*)/'(*) while ( getchar() != '\n' ); } printf("\033[A\033[2K"); // VT100 escape codes } while(1); } void reportSettingsIssues(int sum){ char *setLst[] = {"server", "channel", "port", "nick", "username", "realname", "password", "maxNickLenght", "logPath", "link", "reJoin", "floodTimeOut"}; int i; switch (sum){ case -1: printf("Configuration file issue: value or attribute missed or redundant\n"); break; case -2: printf ("Unable to open configuration files. Please make sure that they exist at one of the next folders\n" "/etc/loperIRCLogBot/bot.conf\n" "./bot.conf\n" "Please note, that configuration file shouldn't be lager then 15 lines.\n" "If files exists, please check that user have read permissions for configuraion file\n"); break; case -3: printf("Configuration file issue: 'logPath' is not defined as absolute path\n"); break; default: printf("Configuration file issue. Next field(s) not found:\n"); for (i=0; i<12; i++) if ((sum & (1< 2){ printf("Too many arguments.\n" "Use --help for information.\n"); return 0; } // -================================================================================================- config = load_config(0, dest); if (config.status == 0) { printf("\t--------------------\n" "\t%s\n" "\t\tSETTINGS\n" "\t--------------------\n" "server - %s\n" "channel - %s\n" "port - %d\n" "nick - %s\n" "username - %s\n" "realname - %s\n" "password - %s\n" "maxNickLength - %d\n" "logPath - %s\n" "link - %s\n" "reJoin - %s\n" "floodTimeOut - %d\n" "\t--------\n", printTimeStamp(), config.server, config.channel, config.port, config.nick, config.username, config.realname, config.password, config.maxNickLength, config.logPath, config.link, config.reJoin == 0?"No":"Yes", config.floodTimeOut); if ( createDir(config.logPath, "logs") != 0){ // set logs directory printf ("Unable to create directory for log files (%s)\n" "Please make sure that you have premissions to write in this directory\n", config.logPath); return 1; } else { // Init callbacks memset ( &callbacks, 0, sizeof(callbacks) ); // Set up the mandatory events callbacks.event_connect = event_connect; callbacks.event_numeric = event_numeric; callbacks.event_join = event_join; // Set up the rest of events callbacks.event_ctcp_req = event_ctcp_req; callbacks.event_notice = event_notice; callbacks.event_ctcp_rep = event_ctcp_rep; // & dump_event? Now no need to dump. This event is triggered upon receipt of an CTCP response. //Thus if you generate the CTCP message and the remote user responded, this event handler will be called. //callbacks.event_unknown = event_unknown; // this event will be triggered when app recieve unknown request from server. callbacks.event_channel = dump_event; callbacks.event_nick = dump_event; callbacks.event_quit = dump_event; callbacks.event_part = dump_event; callbacks.event_mode = dump_event; callbacks.event_topic = dump_event; callbacks.event_kick = dump_event; callbacks.event_invite = dump_event; callbacks.event_umode = dump_event; callbacks.event_ctcp_action = dump_event; // "ACTION" handler // Now create the session while (1) { printf("%s Connecting...\n", printTimeStamp()); session = irc_create_session( &callbacks ); if ( !session ){ printf("%s Unable to create session\n", printTimeStamp()); // return 1; // give a try once again } else { irc_option_set( session, LIBIRC_OPTION_SSL_NO_VERIFY ); // Connect to a regular IRC server if ( irc_connect (session, config.server, config.port, 0, config.nick, config.username, config.realname ) ){ printf ("%s Could not connect: %s\n", printTimeStamp(), irc_strerror (irc_errno(session))); // return 1; //give a try once again // TODO add a counter for few reply and die / do something with this } else { is_alive(1, time(NULL), session); //initialize is_alive internal start time printf("%s Connection established\n", printTimeStamp()); ///////////////////// irc_run() replacement /////////////////// while ( irc_is_connected(session) ) { if (reloadLogSig == 1) //debugFunction logsReopen(); if (terminateSig == 1) //debugFunction killBySignalRecieved(session); ts.tv_sec = 0; ts.tv_nsec = 250000000; // Init sets FD_ZERO (&in_set); FD_ZERO (&out_set); // +1 chk if ( is_alive(0, time(NULL), session) != 0 ){ printf("\n\t-----------------------------------\n\t" "%s PING timeout\n" "\t-----------------------------------\n", printTimeStamp()); break; // 1 } // end +1 chk irc_add_select_descriptors (session, &in_set, &out_set, &maxfd); // Set mask to all signals, while neccessary for us are: SIGHUP, SIGTERM. Exclude SIGINT, 'cause no matter how this app fucks up in case of _exit(2). if ( pselect (maxfd + 1, &in_set, &out_set, 0, &ts, &blockedSignals) < 0 ) // and errno == EINTR ! { printf ("%s Could not connect or I/O error: LIBIRC_ERR_TERMINATED\n", printTimeStamp()); break; // 1 } if ( irc_process_select_descriptors (session, &in_set, &out_set) ){ printf ("%s Could not connect or I/O error\n", printTimeStamp()); break; // 1 } } irc_disconnect(session); printf("%s Connection lost\n", printTimeStamp()); //return 0; //////////////////////////////////////////////////////////////////////////// } } printf ("%s Not connected: will retry in 5 sec\n", printTimeStamp()); sleep (5); } // while end } return 0; // never happens } else { reportSettingsIssues(config.status); return 1; } return 0; }