diff options
| author | Luke Bratch <luke@bratch.co.uk> | 2019-09-15 14:47:44 +0100 | 
|---|---|---|
| committer | Luke Bratch <luke@bratch.co.uk> | 2019-09-15 14:47:44 +0100 | 
| commit | e9d4ad3c33b81ff56c4e4b2cac4aad559a303104 (patch) | |
| tree | c177baee475291dc1545f48c942560351aa92e4f | |
| parent | 4dea4c16313ba3d1575cfa6722d75492c907f551 (diff) | |
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).
| -rw-r--r-- | TODO | 4 | ||||
| -rw-r--r-- | blabouncer.c | 4 | ||||
| -rw-r--r-- | functions.c | 250 | ||||
| -rw-r--r-- | functions.h | 25 | ||||
| -rw-r--r-- | logging.c | 43 | ||||
| -rw-r--r-- | logging.h | 9 | ||||
| -rw-r--r-- | message.c | 60 | ||||
| -rw-r--r-- | structures.h | 2 | 
8 files changed, 391 insertions, 6 deletions
| @@ -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 @@ -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) { @@ -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.  // @@ -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 | 
