/* * 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'. 'ournick' 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 // // With the ":foo!bar@baz "prefix being important for either // type. // // Returns 1 on success or 0 on failure. int logline(char *str, char *ournick, 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; // 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). 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 to ensure filename ends up being safe char from[MAXCHAR], to[MAXCHAR]; strcpy(from, tokens[0]); strcpy(to, tokens[2]); // Remove unsafe characters (assuming POSIX, just strip "/" and replace with "_") replacechar(from, '/', '_'); replacechar(to, '/', '_'); // Ensure filename wouldn't be too long if (strlen(from) > NAME_MAX || strlen(to) > NAME_MAX) { debugprint(DEBUG_CRIT, "Filename would be too long if logging either '%s' or '%s', returning!\n", from, to); return 0; } switch(type) { case LOG_PRIVMSG: // Extract the username from the prefix extractnickfromprefix(tokens[0]); extractnickfromprefix(from); // Remove the leading ":" from the real message stripprefix(str); // Build the log filename // If the message was sent to us, then log it in the sender's log file if (strncmp(tokens[2], ournick, strlen(tokens[0])) == 0) { if (!snprintf(filename, MAXCHAR, "%s/logs/%s.log", basedir, from)) { debugprint(DEBUG_CRIT, "Error while 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 log filename for to name, returning!\n"); return 0; } } 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]); // Remove the leading ":" from the topic stripprefix(str); if (!snprintf(filename, MAXCHAR, "%s/logs/%s.log", basedir, to)) { debugprint(DEBUG_CRIT, "Error while 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; default : printf("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 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) { // 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', length '%ld'.\n", line, 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; }