From e9d4ad3c33b81ff56c4e4b2cac4aad559a303104 Mon Sep 17 00:00:00 2001 From: Luke Bratch Date: Sun, 15 Sep 2019 14:47:44 +0100 Subject: Start tracking nicks in channels (upon JOIN/PART/QUIT/NICK) and use that to correctly log QUITs in the replay log and normal log(s). --- TODO | 4 +- blabouncer.c | 4 + functions.c | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- functions.h | 25 ++++++ logging.c | 43 +++++++++- logging.h | 9 +++ message.c | 60 ++++++++++++++ structures.h | 2 + 8 files changed, 391 insertions(+), 6 deletions(-) diff --git a/TODO b/TODO index 68e0122..32bfa2c 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,3 @@ All the TODOs sprinkled throughout the code! -Is there a way to log nick changes to the normal log despite not tracking nicks in each channel? (We do track channel names themselves.) - -Log QUIT messages in channels if the quitting user was in them - requires tracking users in channels. +Log nick changes to the normal log. diff --git a/blabouncer.c b/blabouncer.c index 772b969..d6df294 100644 --- a/blabouncer.c +++ b/blabouncer.c @@ -438,6 +438,10 @@ void dochat(int *serversockfd, int *clientsockfd, struct settings *settings) { // Set initial channel names to empty strings for (int i = 0; i < MAXCHANNELS; i++) { channels[i].name[0] = '\0'; + // And all the nicks within it + for (int j = 0; j < MAXCHANNICKS; j++) { + channels[i].nicks[j][0] = '\0'; + } } // Initialise OpenSSL (used for both client and server) diff --git a/functions.c b/functions.c index fe96e0a..3dd8045 100644 --- a/functions.c +++ b/functions.c @@ -179,6 +179,7 @@ void appendcrlf(char *string) { } // Remove leading colon ':' which is the starting character of a prefix in an IRC message +// If no leading colon present, string is left unchanged void stripprefix(char *string) { // Make a copy to work with char string2[strlen(string)]; @@ -643,8 +644,11 @@ int createchannel(struct channel *channels, char *name, char *topic, char *topic strncpy(channels[i].topicwhen, topicwhen, strlen(topicwhen)); channels[i].topicwhen[strlen(topicwhen)] = '\0'; channels[i].gotnames = 0; + // Set nicks to blank + for (int j = 0; j < MAXCHANNICKS; j++) { + channels[i].nicks[j][0] = '\0'; + } return 1; - break; // TODO - This should be safe to remove since return is hit first } } @@ -714,6 +718,10 @@ int removechannel(struct channel *channels, char *name) { if (strncmp(channels[i].name, name, strlen(name)) == 0) { // ..and NULL its name (0th character = '\0') channels[i].name[0] = '\0'; + // Set nicks to blank + for (int j = 0; j < MAXCHANNICKS; j++) { + channels[i].nicks[j][0] = '\0'; + } debugprint(DEBUG_FULL, "removechannel(): channel '%s' removed and topicwhen set to '%s'.\n", name, channels[i].topicwhen); return 1; } @@ -1319,3 +1327,243 @@ void replacechar(char *str, char find, char replace) { } } } + +// Add nick (passed as a :nick!user@host) to channel 'channel' +// Returns 1 on success or 0 on failure +int addnicktochannel(char *nickuserhost, char *channel, struct channel *channels) { + debugprint(DEBUG_FULL, "addnicktochannel(): given '%s' and '%s'.\n", nickuserhost, channel); + + // Get the nick from the prefix + extractnickfromprefix(nickuserhost); + + // Make sure the channel exists + int chanfound = 0; + int chanindex; + for (chanindex = 0; chanindex < MAXCHANNELS; chanindex++) { + if (strlen(channels[chanindex].name) == strlen(channel) && !strcmp(channels[chanindex].name, channel)) { + chanfound = 1; + break; + } + } + if (!chanfound) { + debugprint(DEBUG_CRIT, "addnicktochannel(): channel '%s' not found in channel struct.\n", channel); + return 0; + } + + // Add the nick to the channel + for (int i = 0; i < MAXCHANNICKS; i++) { + // Make sure the nick isn't already in the channel struct + if (strlen(channels[chanindex].nicks[i]) == strlen(nickuserhost) && !strcmp(channels[chanindex].nicks[i], nickuserhost)) { + // Unexectedly the nick is already here, hopefully it's OK so let's return 1 + debugprint(DEBUG_FULL, "addnicktochannel(): nick '%s' already in channel '%s', returning.\n", nickuserhost, channel); + return 1; + } + + // Find the first unoccupied slot and put the nick in + if (!channels[chanindex].nicks[i][0]) { + strcpy(channels[chanindex].nicks[i], nickuserhost); + debugprint(DEBUG_FULL, "addnicktochannel(): added nick '%s' to channel '%s'.\n", nickuserhost, channel); + return 1; + } + } + + // We shouldn't get here, return error + debugprint(DEBUG_CRIT, "addnicktochannel(): got to the end of the function without adding nick '%s' to channel '%s', returning error.\n", + nickuserhost, channel); + return 0; +} + +// Remove nick (passed as a :nick!user@host) from channel 'channel' +// Returns 1 on success or 0 on failure +int removenickfromchannel(char *nickuserhost, char *channel, struct channel *channels) { + debugprint(DEBUG_FULL, "removenickfromchannel(): given '%s' and '%s'.\n", nickuserhost, channel); + + // Get the username from the prefix + extractnickfromprefix(nickuserhost); + + // Make sure the channel exists + int chanfound = 0; + int chanindex; + for (chanindex = 0; chanindex < MAXCHANNELS; chanindex++) { + if (strlen(channels[chanindex].name) == strlen(channel) && !strcmp(channels[chanindex].name, channel)) { + chanfound = 1; + break; + } + } + if (!chanfound) { + debugprint(DEBUG_CRIT, "removenickfromchannel(): channel '%s' not found in channel struct.\n", channel); + return 0; + } + + // Remove the nick from the channel + for (int i = 0; i < MAXCHANNICKS; i++) { + // Remove the the nick + if (strlen(channels[chanindex].nicks[i]) == strlen(nickuserhost) && !strcmp(channels[chanindex].nicks[i], nickuserhost)) { + // By null terminating its string + debugprint(DEBUG_FULL, "removenickfromchannel(): nick '%s' removed from channel '%s'.\n", nickuserhost, channel); + channels[chanindex].nicks[i][0] = '\0'; + return 1; + } + } + + // We shouldn't get here, return error + debugprint(DEBUG_CRIT, "removenickfromchannel(): got to the end of the function without removing nick '%s' from channel '%s', returning error.\n", + nickuserhost, channel); + return 0; +} + +// Remove nick (passed as a :nick!user@host) from all channels +// Returns 1 on success or 0 on failure +int removenickfromallchannels(char *nickuserhost, struct channel *channels) { + debugprint(DEBUG_FULL, "removenickfromallchannels(): given '%s'.\n", nickuserhost); + + // Get the nick from the prefix + extractnickfromprefix(nickuserhost); + + // Go through all channels and remove nick if present + for (int i = 0; i < MAXCHANNELS; i++) { + // Go through all nicks in channel + for (int j = 0; j < MAXCHANNICKS; j++) { + // Remove the nick from the channel if present + if (strlen(channels[i].nicks[j]) == strlen(nickuserhost) && !strcmp(channels[i].nicks[j], nickuserhost)) { + // By null terminating its string + channels[i].nicks[j][0] = '\0'; + debugprint(DEBUG_FULL, "removenickfromallchannels(): nick '%s' removed from channel '%s'.\n", nickuserhost, channels[i].name); + } + } + } + + return 1; +} + +// Update old nick (passed as a :nick!user@host) to 'newnick' in all channels +// Returns 1 on success or 0 on failure +int updatenickinallchannels(char *nickuserhost, char *newnick, struct channel *channels) { + debugprint(DEBUG_FULL, "updatenickinallchannels(): given '%s' and '%s'.\n", nickuserhost, newnick); + + // Get the nick from the prefix + extractnickfromprefix(nickuserhost); + + // Strip prefix from newnick + stripprefix(newnick); + + // Go through all channels and update nick if present + for (int i = 0; i < MAXCHANNELS; i++) { + // Go through all nicks in channel + for (int j = 0; j < MAXCHANNICKS; j++) { + // Update the nick in the channel if present + if (strlen(channels[i].nicks[j]) == strlen(nickuserhost) && !strcmp(channels[i].nicks[j], nickuserhost)) { + strcpy(channels[i].nicks[j], newnick); + debugprint(DEBUG_FULL, "updatenickinallchannels(): nick '%s' updated to '%s' in channel '%s'.\n", nickuserhost, newnick, channels[i].name); + } + } + } + + return 1; +} + +// Populate our channels struct with all nicks in a RPL_NAMREPLY +// Returns 1 on success or 0 on failure +int addnamereplytochannel(char *namereply, struct channel *channels) { +//:irc.tghost.co.uk 353 blabounce = #blabouncer :blabounce bbnick ~@l_bratch @l_blabnc Hughbla Bratchbot ars + debugprint(DEBUG_FULL, "addnamereplytochannel(): given '%s'.\n", namereply); + + // Make a copy since we don't need to modify the original + char strcopy[MAXDATASIZE]; + strcpy(strcopy, namereply); + + // Strip the leading ':' + stripprefix(strcopy); + + // Find the start of the channel name, which comes after the first '=' followed by a space + int channelpos = -1; + for (size_t i = 0; i < strlen(strcopy) - 2; i++) { + if (strcopy[i] == '=' && strcopy[i + 1] == ' ' && strcopy[i + 2] != '\0') { + // Name found + channelpos = i + 2; + break; + } + } + if (channelpos == -1) { + // Didn't find the name, abort + debugprint(DEBUG_FULL, "addnamereplytochannel(): couldn't find start of channel name in '%s'.\n", namereply); + return 0; + } + + // Find the end of the channel name + char channelname[MAXCHANLENGTH]; + for (size_t i = channelpos; i < strlen(strcopy); i++) { + // Stop when a space is found or if we're going to exceed MAXCHANLENGTH + if (strcopy[i] == ' ' || i - channelpos == MAXCHANLENGTH - 2) { + break; + } + channelname[i - channelpos] = strcopy[i]; + channelname[i - channelpos + 1] = '\0'; + } + + // Start with a nice clean string that just consists of nicks at the end of the string + char nickstr[MAXDATASIZE]; + strcpy(nickstr, strcopy + channelpos + strlen(channelname) + 1); + + // Split nickstr up into its space-separated nick components + + // Copy to a temporary string for feeding to strsep + char *nickcopy = strdup(nickstr); + // Keep track of initial pointer for free()ing later + char *nickcopyPtr = nickcopy; + + // Track which CLRF-separated nick we're on + int nickcount = 0; + // Build array of each space-separated token + char nicks[MAXTOKENS][MAXDATASIZE]; + // Split the string by ' ' and add each space-separated nick to an array + char *token; + while ((token = strsep(&nickcopy, " ")) != NULL) { + if (*token == '\0') continue; // Skip consecutive matches + if (nickcount >= MAXTOKENS) break; // Too many tokens + debugprint(DEBUG_FULL, "addnamereplytochannel(): Token: '%s', length '%ld'.\n", token, strlen(token)); + // Make sure it's not too long + if (strlen(token) > MAXNICKLENGTH - 1) { + debugprint(DEBUG_CRIT, "addnamereplytochannel(): nick too long, discarding.\n"); + continue; + } + // Copy into the token array (strlen + 1 to get the NULL terminator) + strncpy(nicks[nickcount], token, strlen(token) + 1); + nickcount++; + } + + free(nickcopyPtr); + + // Clean up each nick (remove prefixes and such) + for (int i = 0; i < nickcount; i++) { + stripprefixesfromnick(nicks[i]); + // And add to the channel + addnicktochannel(nicks[i], channelname, channels); + } + + return 1; +} + +// Strips all leading prefixes (colons, user modes) from a nick +void stripprefixesfromnick(char *nick) { + debugprint(DEBUG_FULL, "stripprefixesfromnick(): given '%s'.\n", nick); + + char nicktmp[MAXNICKLENGTH]; + int pos = 0; + + for (size_t i = 0; i < strlen(nick); i++) { + // Only copy non-prefix chars + if (nick[i] != ':' && nick[i] != '~' && nick[i] != '&' && nick[i] != '@' && nick[i] != '%' && nick[i] != '+') { + nicktmp[pos] = nick[i]; + pos++; + } + } + + // Null terminate + nicktmp[pos] = '\0'; + + debugprint(DEBUG_FULL, "stripprefixesfromnick(): produced '%s'.\n", nicktmp); + + // Copy back to source string + strcpy(nick, nicktmp); +} diff --git a/functions.h b/functions.h index 5afdca0..614137d 100644 --- a/functions.h +++ b/functions.h @@ -53,6 +53,8 @@ #define MAXCHANNELS 1024 // Let's assume 1024 is reasonable for now (it's configured per IRCd) #define MAXRFCNICKLEN 9 // From RFC 1459 +#define MAXTOKENS 100 // For strsep string splitting + #define VERSION "0.1.1" // Blabouncer version // Write debug string to file. @@ -67,6 +69,7 @@ int getstdin(char *prompt, char *buff, size_t sz); void appendcrlf(char *string); // Remove leading colon ':' which is the starting character of a prefix in an IRC message +// If no leading colon present, string is left unchanged void stripprefix(char *string); // Extract final parameter from IRC message, removing the leading colon ':' @@ -183,4 +186,26 @@ int getclientcodetime(char *code, struct clientcodes *clientcodes); // Replace any instances of "find" with "replace" in the string "str" void replacechar(char *str, char find, char replace); +// Add nick (passed as a :nick!user@host) to channel 'channel' +// Returns 1 on success or 0 on failure +int addnicktochannel(char *nickuserhost, char *channel, struct channel *channels); + +// Remove nick(passed as a :nick!user@host) from channel 'channel' +// Returns 1 on success or 0 on failure +int removenickfromchannel(char *nickuserhost, char *channel, struct channel *channels); + +// Remove nick (passed as a :nick!user@host) from all channels +// Returns 1 on success or 0 on failure +int removenickfromallchannels(char *nickuserhost, struct channel *channels); + +// Update old nick (passed as a :nick!user@host) to 'newnick' in all channels +// Returns 1 on success or 0 on failure +int updatenickinallchannels(char *nickuserhost, char *newnick, struct channel *channels); + +// Populate our channels struct with all nicks in a RPL_NAMREPLY +// Returns 1 on success or 0 on failure +int addnamereplytochannel(char *namereply, struct channel *channels); + +// Strips all leading prefixes (colons, user modes) from a nick +void stripprefixesfromnick(char *nick); #endif diff --git a/logging.c b/logging.c index ff41911..f6fa23e 100644 --- a/logging.c +++ b/logging.c @@ -40,6 +40,14 @@ // If LOG_NETWORK then it just logs the string verbatim in // a file named 'ircdstate->ircdname'.log. // +// If LOG_QUIT then it expects a string in the format: +// channelname :nick!bar@baz QUIT :bla bla bla +// 'channelname' probably has to be prepended manually by the +// caller since it doesn't feature in the raw message from +// the IRCd. We need it in logline() to log to the relevant +// channel log file. The caller probably has to call logline() +// multiple times for each channel the nick was in. +// // With the ":foo!bar@baz "prefix being important for all // types. // @@ -224,6 +232,37 @@ int logline(char *str, struct ircdstate *ircdstate, char *basedir, int type) { break; + case LOG_QUIT: + // Build a friendly message (e.g. "#channel :nick!user@host QUIT :foo bar baz" -> "nick (user@host) has quit (foo bar baz)") + + // Find the bang in the prefix + // (ret and posbang defined above in case LOG_JOINPART) + if ((ret = strstr(tokens[1], "!")) != NULL) { + // Position within str of "!" + posbang = ret - tokens[1]; + } else { + // No idea what happened, let's abandon ship + debugprint(DEBUG_CRIT, "logline(): Unable to find '!' within nick!user@host, returning!\n"); + return 0; + } + + // Make it a null character + tokens[1][posbang] = '\0'; + + // Strip the prefix from the quit message + stripprefix(str); + + // Build a friendly message (e.g. "nick (user@host) has quit (Quit: message)") + snprintf(line, MAXCHAR, "%s (%s) has quit (%s)", tokens[1] + 1, tokens[1] + posbang + 1, str); + + // Build the log filename + if (!snprintf(filename, MAXCHAR, "%s/logs/%s.log", basedir, tokens[0])) { + debugprint(DEBUG_CRIT, "logline(): Error while preparing log filename for quit, returning!\n"); + return 0; + } + + break; + default : debugprint(DEBUG_CRIT, "logline(): Unknown log type '%d', returning 0.\n", type); return 0; @@ -284,7 +323,7 @@ int logline(char *str, struct ircdstate *ircdstate, char *basedir, int type) { return 0; } } - } else if (type == LOG_JOINPART || type == LOG_TOPIC || type == LOG_NETWORK) { + } else if (type == LOG_JOINPART || type == LOG_TOPIC || type == LOG_NETWORK || type == LOG_QUIT) { // Prepend the time string char line2[MAXCHAR]; if (!snprintf(line2, MAXCHAR, "%s %s", timestr, line)) { @@ -300,7 +339,7 @@ int logline(char *str, struct ircdstate *ircdstate, char *basedir, int type) { // Ensure the line finishes with CRLF appendcrlf(line); - debugprint(DEBUG_FULL, "logline(): Complete log string to write: '%s', length '%ld'.\n", line, strlen(line)); + debugprint(DEBUG_FULL, "logline(): Complete log string to write: '%s' to '%s', length '%ld'.\n", line, filename, strlen(line)); // Write complete line to file if ((bytes = fprintf(fp, "%s", line)) < 0) { diff --git a/logging.h b/logging.h index 6371f8a..726a490 100644 --- a/logging.h +++ b/logging.h @@ -37,6 +37,7 @@ #define LOG_JOINPART 1 #define LOG_TOPIC 2 #define LOG_NETWORK 3 +#define LOG_QUIT 4 #define DEBUG_CRIT 0 #define DEBUG_SOME 1 #define DEBUG_FULL 2 @@ -64,6 +65,14 @@ // If LOG_NETWORK then it just logs the string verbatim in // a file named 'ircdstate->ircdname'.log. // +// If LOG_QUIT then it expects a string in the format: +// channelname :nick!bar@baz QUIT :bla bla bla +// 'channelname' probably has to be prepended manually by the +// caller since it doesn't feature in the raw message from +// the IRCd. We need it in logline() to log to the relevant +// channel log file. The caller probably has to call logline() +// multiple times for each channel the nick was in. +// // With the ":foo!bar@baz "prefix being important for all // types. // diff --git a/message.c b/message.c index ed27173..869a1ce 100644 --- a/message.c +++ b/message.c @@ -197,6 +197,11 @@ int processservermessage(SSL *server_ssl, char *str, struct client *clients, int debugprint(DEBUG_FULL, "Server JOIN: nick is NOT ours ('%s' vs '%s').\n", prefixcopy, ircdstate->ircnick); } + // Add the JOINing nick to our local channel struct + if (!addnicktochannel(tokens[0], tokens[2], channels)) { + debugprint(DEBUG_CRIT, "Failed to add nick to channel struct.\n"); + } + // And then send to all clients sendtoallclients(clients, str, sourcefd, settings); @@ -231,6 +236,11 @@ int processservermessage(SSL *server_ssl, char *str, struct client *clients, int debugprint(DEBUG_FULL, "Server PART: nick is NOT ours ('%s' vs '%s').\n", prefixcopy, ircdstate->ircnick); } + // Remove the PARTing nick from our local channel struct + if (!removenickfromchannel(tokens[0], tokens[2], channels)) { + debugprint(DEBUG_CRIT, "Failed to remove nick from channel struct.\n"); + } + // And then send to all clients sendtoallclients(clients, str, sourcefd, settings); @@ -248,6 +258,45 @@ int processservermessage(SSL *server_ssl, char *str, struct client *clients, int return 1; } + // Server QUIT received? Tell all clients and also remove the user from our local channels struct. + if (strncmp(tokens[1], "QUIT", strlen(tokens[1])) == 0) { + debugprint(DEBUG_FULL, "Server QUIT found and it is: %s with length %zd! Next token is '%s'.\n", tokens[0], strlen(tokens[0]), tokens[2]); + + // And then send to all clients + sendtoallclients(clients, str, sourcefd, settings); + + // Write to replay log if replay logging enabled + if (settings->replaylogging) { + writereplayline(str, settings->basedir); + } + + // Get each channel the QUITting user was in, and log the quit from that channel + if (settings->logging) { + char quitnick[MAXNICKLENGTH]; + strcpy(quitnick, tokens[0]); + extractnickfromprefix(quitnick); + for (int i = 0; i < MAXCHANNELS; i++) { + if (channels[i].name[0]) { + for (int j = 0; j < MAXCHANNICKS; j++) { + if (strlen(channels[i].nicks[j]) == strlen(quitnick) && !strcmp(channels[i].nicks[j], quitnick)) { + char logstring[MAXDATASIZE]; + snprintf(logstring, MAXDATASIZE, "%s %s", channels[i].name, str); + logline(logstring, ircdstate, settings->basedir, LOG_QUIT); + break; + } + } + } + } + } + + // Remove the QUITting nick from our local channel struct + if (!removenickfromallchannels(tokens[0], channels)) { + debugprint(DEBUG_CRIT, "Failed to remove nick from channel structs.\n"); + } + + return 1; + } + // Channel topics/names/nicks/etc. // Server 331 (RPL_NOTOPIC) the topic is blank which we track by having a set timestamp of 0 if (strncmp(tokens[1], "331", strlen(tokens[1])) == 0) { @@ -288,6 +337,12 @@ int processservermessage(SSL *server_ssl, char *str, struct client *clients, int } } } + + // Update our local channels struct with all nicks from this RPL_NAMREPLY + if (!addnamereplytochannel(str, channels)) { + debugprint(DEBUG_CRIT, "Failed to add RPL_NAMREPLY to channels.\n"); + } + return 1; // Server 366 (RPL_ENDOFNAMES), relay to all clients if we've just JOINed the channel, or relay to // and decrement from any clients who were waiting on RPL_NAMREPLY if it's an existing channel. @@ -420,6 +475,11 @@ int processservermessage(SSL *server_ssl, char *str, struct client *clients, int free(prefixcopy); } + // Update old nick to the new nick in our local channel struct + if (!updatenickinallchannels(tokens[0], tokens[2], channels)) { + debugprint(DEBUG_CRIT, "Failed to update old nick to new nick in channels.\n"); + } + // Relay to all clients sendtoallclients(clients, str, sourcefd, settings); diff --git a/structures.h b/structures.h index 8d68083..02b13b8 100644 --- a/structures.h +++ b/structures.h @@ -30,6 +30,7 @@ #define CLIENTCODELEN 17 // Max length of a client code + 1 for null #define MAXCLIENTCODES 64 // Max number of client codes to track #define MAXCONFARR 10 // Max number of entries that a configuration array can have +#define MAXCHANNICKS 8192 // Maximum number of nicks to track per channel struct ircdstate { char greeting001[MAXDATASIZE]; @@ -118,6 +119,7 @@ struct channel { // TODO - Make this an int? It's just going to arrive and leave as a string every time anyway... char topicwhen[11]; // 32-bit unixtime is up to 10 characters (+1 for null char) We use "0" to mean "not set". int gotnames; // Have we finished getting the RPL_NAMREPLYs for this channel yet? + char nicks[MAXCHANNICKS][MAXNICKLENGTH]; // Nicks in the channel to track things like nick changes and quits for log files }; #endif -- cgit v1.2.3