/* * This file is part of blabouncer (https://www.blatech.co.uk/l_bratch/blabouncer). * Copyright (C) 2019 Luke Bratch . * * Blabouncer is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * Blabouncer is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with blabouncer. If not, see . */ #include "logging.h" // Write the line 'str' to the relevant log file such as // '#channel.log' or 'nickname.log'. 'ircdstate->ircnick' // is our own nick and is used to determine which log file // to write to if the type is LOG_PRIVMSG. // 'basedir' is the directory in which the 'logs' directory // will be created in which logs are to be written. // // If LOG_PRIVMSG then it expects a string in the format: // :from!bar@baz PRIVMSG to :hello world // // LOG_PRIVMSG is also used for NOTICEs. // // If LOG_JOINPART then it expects a string in the format: // :nick!bar@baz JOIN :#channel // or // :nick!bar@baz PART #channel // // If LOG_TOPIC then it expects a string in the format: // :nick!bar@baz TOPIC #channel :bla bla bla // // 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. // // If LOG_NICK then it expects a string in the format: // :oldnick!bar@baz NICK :newnick // Same manual 'channelname' prepending as LOG_QUIT above. // // If LOG_MODE then it expects a string in the format: // :nick!bar@baz MODE #channel foo bar [foo bar...] // // With the ":foo!bar@baz "prefix being important for all // types. // // Returns 1 on success or 0 on failure. int logline(char *str, struct ircdstate *ircdstate, char *basedir, int type) { // Filename to write to, gets built as we go char filename[MAXCHAR]; // Log line to ultimately write, gets built as we go char line[MAXCHAR]; // Variables for LOG_JOINPART (can't define directly inside switch case) int pos; // Build array of each space-separated token char tokens[MAXTOKENS][MAXDATASIZE]; char *token; // Whether or not this is a "/me" PRIVMSG so we can log it slightly differently later int meprivmsg = 0; // Split out the first three space-separated parts of the string, leaving the rest. // If LOG_PRIVMSG: // This gets us the prefix (containing the "from" nick), the PRIVMSG command (not needed), // and the "to" nick or channel. Plus the rest of the string intact (which is the actual // message). // If LOG_JOINPART: // This gets us the prefix (containing the joined/parted nick), the JOIN/PART command (not needed), // and the channel name. // If LOG_TOPIC: // This gets us the prefix (containing the topic setting nick), the TOPIC command (not needed), // the channel whose topic was set, and the rest of the string intact (which is the new topic). // If LOG_NETWORK: // Don't do this at all since we want to log the whole string. if (type != LOG_NETWORK) { for (int i = 0; i < 3; i++) { // Try to split if ((token = strsep(&str, " ")) == NULL) { debugprint(DEBUG_CRIT, "Error splitting string for logging, returning!\n"); return 0; } // Copy into the token array (strlen + 1 to get the NULL terminator) strncpy(tokens[i], token, strlen(token) + 1); debugprint(DEBUG_FULL, "logline(): extracted '%s'.\n", tokens[i]); } } // Make "filename safe" copies of from and to names, as well as ircdstate->ircdname // to ensure filename ends up being safe char from[MAXCHAR], to[MAXCHAR], ircdname[MAXCHAR]; strcpy(from, tokens[0]); strcpy(to, tokens[2]); strcpy(ircdname, ircdstate->ircdname); // Remove unsafe characters (assuming POSIX, just strip "/" and replace with "_") replacechar(from, '/', '_'); replacechar(to, '/', '_'); replacechar(ircdname, '/', '_'); // Ensure filename wouldn't be too long (+ 4 for ".log") if (strlen(from) + 4 > NAME_MAX || strlen(to) + 4 > NAME_MAX || strlen(ircdname) + 4 > NAME_MAX ) { debugprint(DEBUG_CRIT, "Filename would be too long if logging either '%s', '%s' or '%s', returning!\n", from, to, ircdname); return 0; } // Make the filename lowercase (since IRC nicks and channel names are case-insensitive, // we can ensure a nick/channel with varying case always ends up in the same log file) strlower(from); strlower(to); strlower(ircdname); switch(type) { case LOG_PRIVMSG: // Extract the username from the prefix extractnickfromprefix(tokens[0], 1); extractnickfromprefix(from, 1); // Remove the leading ":" from the real message stripprefix(str, 1); // Build the log filename // If the message was sent to us, then log it in the sender's log file if ((strlen(tokens[2]) == strlen(ircdstate->ircnick)) && (strncmp(tokens[2], ircdstate->ircnick, strlen(tokens[2])) == 0)) { if (!snprintf(filename, MAXCHAR, "%s/logs/%s.log", basedir, from)) { debugprint(DEBUG_CRIT, "Error while preparing log filename for from name, returning!\n"); return 0; } } else { // Otherwise log it in the "to" log file if (!snprintf(filename, MAXCHAR, "%s/logs/%s.log", basedir, to)) { debugprint(DEBUG_CRIT, "Error while preparing log filename for to name, returning!\n"); return 0; } } // If it was a "/me" line then strip the "\1ACTION " and the trailing "\1", plus set a flag for later if (strstr(str, "\1ACTION ") == str) { debugprint(DEBUG_FULL, "logline(): /me PRIVMSG detected, stripping ACTION things.\n"); // Skip over the first 8 characters (\1ACTION ) str += 8; // And remove the trailing \1 if (str[strlen(str) - 1] == '\1') { str[strlen(str) - 1] = '\0'; } meprivmsg = 1; } debugprint(DEBUG_FULL, "logline(): Logging PRIVMSG from '%s' to '%s' message '%s' in filename '%s'.\n", tokens[0], tokens[2], str, filename); break; case LOG_JOINPART: // Find the start of the channel name // If it's a JOIN if (tokens[2][0] == ':') { pos = 1; } else if (tokens[2][0] == '#') { // Perhaps it's a PART pos = 0; } else { // If not found, return 0 return 0; } snprintf(filename, MAXCHAR, "%s/logs/%s.log", basedir, to + pos); debugprint(DEBUG_FULL, "logline(): Logging JOIN/PART to/from '%s' in filename '%s'.\n", tokens[2] + pos, filename); // Build a friendly message (e.g. ":nick!user@host JOIN #channel" -> "nick (user@host) has joined #channel") // Find the bang in the prefix char *ret; int posbang; if ((ret = strstr(tokens[0], "!")) != NULL) { // Position within str of "!" posbang = ret - tokens[0]; } else { // No idea what happened, let's abandon ship return 0; } // Make it a null character tokens[0][posbang] = '\0'; // Swap JOINed or PARTed for a friendly word if (strncmp(tokens[1], "JOIN", strlen("JOIN")) == 0) { snprintf(tokens[1], strlen("joined") + 1, "joined"); } else if (strncmp(tokens[1], "PART", strlen("PART")) == 0) { snprintf(tokens[1], strlen("left") + 1, "left"); } // Copy the nick, then user@host, then whether it was a join or part, then the channel name the string to send snprintf(line, MAXCHAR, "%s (%s) has %s %s", tokens[0] + 1, tokens[0] + posbang + 1, tokens[1], tokens[2] + pos); break; case LOG_TOPIC: // Extract the username from the prefix extractnickfromprefix(tokens[0], 1); // Remove the leading ":" from the topic stripprefix(str, 1); if (!snprintf(filename, MAXCHAR, "%s/logs/%s.log", basedir, to)) { debugprint(DEBUG_CRIT, "Error while preparing log filename for topic, returning!\n"); return 0; } debugprint(DEBUG_FULL, "logline(): Logging TOPIC for '%s' in filename '%s'.\n", tokens[2], filename); // Build a friendly message (e.g. ":nick!user@host TOPIC #channel :blah blah" -> "nick has changed the topic to: blah blah") snprintf(line, MAXCHAR, "%s has changed the topic to: %s", tokens[0], str); break; case LOG_NETWORK: // Make sure we actually have an ircdname, if not, don't try to continue if (strlen(ircdstate->ircdname) < 1) { debugprint(DEBUG_CRIT, "logline(): No ircdname (yet?), returning.\n"); return 0; } if (!snprintf(filename, MAXCHAR, "%s/logs/%s.log", basedir, ircdname)) { debugprint(DEBUG_CRIT, "Error while preparing log filename for network, returning!\n"); return 0; } debugprint(DEBUG_FULL, "logline(): Logging network log for '%s' in filename '%s'.\n", str, filename); // Copy source message verbatim to destination strcpy(line, str); 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, 1); // 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, from)) { debugprint(DEBUG_CRIT, "logline(): Error while preparing log filename for quit, returning!\n"); return 0; } break; case LOG_NICK: // Extract old nick from the prefix extractnickfromprefix(tokens[1], 1); // Strip colon from new nick stripprefix(str, 1); // Build a friendly message (e.g. "oldnick is now known as newnick") snprintf(line, MAXCHAR, "%s is now known as %s", tokens[1], str); // Build the log filename if (!snprintf(filename, MAXCHAR, "%s/logs/%s.log", basedir, from)) { debugprint(DEBUG_CRIT, "logline(): Error while preparing log filename for nick, returning!\n"); return 0; } break; case LOG_MODE: // Extract nick from prefix extractnickfromprefix(tokens[0], 1); // Build a friendly message (e.g. "nick sets mode #channel +ov nick1 nick2") if (!snprintf(line, MAXCHAR, "%s sets mode %s %s", tokens[0], tokens[2], str)) { debugprint(DEBUG_CRIT, "logline(): Error while preparing friendly message for mode log message, returning!\n"); return 0; } // Build the log filename if (!snprintf(filename, MAXCHAR, "%s/logs/%s.log", basedir, to)) { debugprint(DEBUG_CRIT, "logline(): Error while preparing log filename for mode, returning!\n"); return 0; } break; default : debugprint(DEBUG_CRIT, "logline(): Unknown log type '%d', returning 0.\n", type); return 0; } // Make sure the log directory exists char logdir[PATH_MAX]; snprintf(logdir, PATH_MAX, "%s/logs/", basedir); struct stat st = {0}; if (stat(logdir, &st) == -1) { if (mkdir(logdir, 0700)) { debugprint(DEBUG_CRIT, "Error creating log directory '%s.\n", logdir); printf("Error creating log directory '%s'.\n", logdir); return 0; } else { debugprint(DEBUG_FULL, "logline(): log directory '%s'.\n", logdir); } } FILE *fp; int bytes = 0; fp = fopen(filename, "a"); if (fp == NULL) { debugprint(DEBUG_CRIT, "error: could not open log file '%s' for writing.\n", filename); printf("error: could not open log file '%s' for writing.\n", filename); fclose(fp); return 0; } // Get a current time string to prepend - TODO - Make this customisable. time_t rawtime; struct tm * timeinfo; time(&rawtime); timeinfo = localtime(&rawtime); // Strip the trailing newline char timestr[MAXCHAR]; snprintf(timestr, MAXCHAR, "%s", asctime(timeinfo)); timestr[strlen(timestr) - 1] = '\0'; if (type == LOG_PRIVMSG) { // Prepend the time string and "from" nick, different formatting depending on // whether or not it was a "/me" message if (meprivmsg) { if (!snprintf(line, MAXCHAR, "%s * %s %s", timestr, tokens[0], str)) { fprintf(stderr, "Error while preparing /me log string to write!\n"); debugprint(DEBUG_CRIT, "Error while preparing /me log string to write!\n"); fclose(fp); return 0; } } else { if (!snprintf(line, MAXCHAR, "%s <%s> %s", timestr, tokens[0], str)) { fprintf(stderr, "Error while preparing log string to write!\n"); debugprint(DEBUG_CRIT, "Error while preparing log string to write!\n"); fclose(fp); return 0; } } } else if (type == LOG_JOINPART || type == LOG_TOPIC || type == LOG_NETWORK || type == LOG_QUIT || type == LOG_NICK || type == LOG_MODE) { // Prepend the time string char line2[MAXCHAR]; if (!snprintf(line2, MAXCHAR, "%s %s", timestr, line)) { fprintf(stderr, "Error while preparing log string to write!\n"); debugprint(DEBUG_CRIT, "Error while preparing log string to write!\n"); fclose(fp); return 0; } // Copy back to line to write snprintf(line, MAXCHAR, "%s", line2); } // Ensure the line finishes with CRLF appendcrlf(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) { printf("error: could not write to log file.\n"); debugprint(DEBUG_CRIT, "error: could not write to log file.\n"); fclose(fp); return 0; } fclose(fp); return bytes; }