Commit e9d4ad3c authored by Luke Bratch's avatar Luke Bratch
Browse files

Start tracking nicks in channels (upon JOIN/PART/QUIT/NICK) and use that to...

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).
parent 4dea4c16
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.
......@@ -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)
......
......@@ -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);
}
......@@ -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);
......
......@@ -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
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment