/* * 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 "replay.h" // Return the unixtime timestamp at the start of a line, // or -1 if a timestamp couldn't be extracted int gettimestamp(char *str) { int timestamp; char timestr[TIMELEN]; int count = 0; // Make sure we're starting with a digit if (!isdigit(str[0])) { return -1; } // Extract each digit until we encounter a non-digit for (int i = 0; i < TIMELEN; i++) { if (isdigit(str[i])) { timestr[count] = str[i]; count++; } } timestr[count] = '\0'; // Null terminate timestamp = strtol(timestr, NULL, 10); // Convert resulting string to an integer, base 10 return timestamp; } // Set 'str' to a string with the leading unixtime timestamp removed. // Returns 1 on success, 0 on failure int striptimestamp(char *str) { char line[MAXCHAR]; int count = 0; // Make sure we're starting with a digit if (!isdigit(str[0])) { return 0; } // Skip over each digit until we encounter a non-digit, record the position for (int i = 0; i < TIMELEN; i++) { if (isdigit(str[i])) { count++; } } // Skip over the space count++; int count2 = 0; // Copy from the end of the digits (and the space) until the end of the string for (size_t i = count; i < strlen(str); i++) { line[count2] = str[i]; count2++; } strncpy(str, line, count2); str[count2] = '\0'; return 1; } // Take a string like: // 1557592901 :foo!bar@baz PRIVMSG foo :hello world // And convert it to: // :foo!bar@baz PRIVMSG foo :[17:41:41] hello world // Or the same but e.g. [DD/MM/YY HH:MM:SS] if replaydates == 1. // Only inserts the formatted time for PRIVMSGs at the moment (and maybe only needs to!). void formattime(char *str, int replaydates) { // Extract the timestamp for conversion into [HH:MM:SS] char timestr[TIMELEN]; sprintf(timestr, "%d", gettimestamp(str)); // Convert int time to string time struct tm tm; strptime(timestr, "%s", &tm); char timestampf[DATETIMELEN]; // Formatted timestamp // Convert into [HH:MM:SS] or e.g. [DD/MM/YY HH:MM:SS] if (replaydates) { strftime(timestampf, DATETIMELEN, "[%x %H:%M:%S]", &tm); } else { strftime(timestampf, DATETIMELEN, "[%H:%M:%S]", &tm); } // Strip the original unixtimestamp striptimestamp(str); // Take note of the length int len = strlen(str); // Find the start of the message if it's a PRIVMSG // TODO - What if this string happens to appear in a non-PRIVMSG? char *ret; int pos1; if ((ret = strstr(str, "PRIVMSG")) != NULL) { // Position within str of "PRIVMSG" pos1 = ret - str; } else { // If it's not a PRIVMSG, stop here return; } // Make sure it's not a "/me" line // TODO - What if this string happens to appear in a non-PRIVMSG? if (strstr(str, " :\1ACTION ")) { // If so, stop here return; } char *ret2; int pos2; // Find the start of the actual message within the PRIVMSG // TODO - What if it's some other message with a colon in it? Find PRIVMSG or not properly. if ((ret2 = strstr(ret, ":")) != NULL) { // Position within ret of ":" pos2 = ret2 - ret; } else { // Didn't find the real message, weird. return; } // Position of start of PRIVMSG colon in original string int realpos = pos1 + pos2 + 1; // Build the new formatted string char newline[MAXCHAR]; // First bit (:foo!bar@baz PRIVMSG foo :) for (int i = 0; i < realpos; i++) { newline[i] = str[i]; } // Second bit (:foo!bar@baz PRIVMSG foo :[HH:MM:SS]) int j = 0; for (size_t i = realpos; i < strlen(timestampf) + realpos; i++) { newline[i] = timestampf[j]; j++; } // Insert a space after the formatted timestamp newline[strlen(timestampf) + realpos] = ' '; newline[strlen(timestampf) + realpos + 1] = '\0'; // Record length of temporary newline string and original real message (after the colon) size_t newlinelen = strlen(newline); size_t msglen = strlen(str) - realpos - 2; // - 2 to ignore leading colon and null terminator // Append the real message to the temporary newline string for (size_t i = 0; i < strlen(str) - realpos - 2; i++) { newline[newlinelen + i] = str[realpos + i]; } // Null terminate it newline[newlinelen + msglen] = '\0'; // Copy the whole thing back to str and null terminate strncpy(str, newline, len + strlen(timestampf)); str[len + strlen(timestampf)] = '\0'; } // Sanitise a potential replay string 'str' by detecting if replay line is any of: // 1. TOPIC/JOIN/PART but we're not in the channel any more // 2. TOPIC/JOIN/PART and it was from us // 3. NICK from us but not our current nick // Returns 1 if the replay line should go through as is // Returns 0 if the replay line should be skipped // Returns -1 on error int sanitisereplay (char *str, struct ircdstate *ircdstate, struct channel *channels) { // Check if the replay line is a TOPIC, a JOIN, or a PART so we don't // replay those if we are not currently in the channel they are from // otherwise clients and state go a bit mad. // Never replay them if they are from us. // Copy to a temporary string char *strcopy = strdup(str); // Keep track of initial pointer for free()ing later char *strcopyPtr = strcopy; // Build array of each space-separated token char tokens[3][MAXDATASIZE]; char *token; for (int i = 0; i < 3; i++) { // Try to split if ((token = strsep(&strcopy, " ")) == NULL) { debugprint(DEBUG_CRIT, "sanitisereplay(): error splitting string on iteration '%d', returning!\n", i); free(strcopyPtr); return -1; } // Copy into the token array (strlen + 1 to get the NULL terminator) strncpy(tokens[i], token, strlen(token) + 1); } if (strncmp(tokens[1], "TOPIC", strlen("TOPIC")) == 0 || strncmp(tokens[1], "JOIN", strlen("JOIN")) == 0 || strncmp(tokens[1], "PART", strlen("PART")) == 0) { // Skip over colon if present in channel name int offset = 0; if (tokens[2][0] == ':') { offset = 1; } // To make sure it's not us extractnickfromprefix(tokens[0], 1); // Check if we're currently in this channel or if the log line is from us if (!inchannel(channels, ircdstate->maxchannelcount, tokens[2] + offset) || ((strlen(tokens[0]) == strlen(ircdstate->ircnick)) && (strncmp(tokens[0], ircdstate->ircnick, strlen(tokens[0])) == 0))) { debugprint(DEBUG_FULL, "sanitisereplay(): Not sending '%s' replay line '%s'.\n", tokens[1], str); free(strcopyPtr); return 0; } } // Separate special check for if a NICK change is from us but it isn't our current nick if (strncmp(tokens[1], "NICK", strlen("NICK")) == 0) { extractnickfromprefix(tokens[0], 1); if ((strlen(tokens[0]) == strlen(ircdstate->ircnick)) && (strncmp(tokens[0], ircdstate->ircnick, strlen(tokens[0])) == 0)) { debugprint(DEBUG_FULL, "sanitisereplay(): Not sending '%s' replay line '%s'.\n", tokens[1], str); free(strcopyPtr); return 0; } } free(strcopyPtr); return 1; } // Return the number of lines in the replay log since 'seconds' seconds ago, or -1 if there a problem. // 'basedir' is the directory in which to find 'replay.log'. int replaylinestime(int seconds, char *basedir) { FILE *fp; char str[MAXCHAR]; char filename[PATH_MAX]; // Build path snprintf(filename, PATH_MAX, "%s/replay.log", basedir); int numlines = 0; fp = fopen(filename, "r"); if (fp == NULL) { debugprint(DEBUG_FULL, "error: could not open replay log '%s'.\n", filename); // Assume the file just doesn't exist yet - TODO - Interpret error codes to see what happened. return 0; } // Get the current time for comparison later int timenow = (int)time(NULL); while (fgets(str, MAXCHAR, fp) != NULL) { // Read the timestamp from each line int timestamp = gettimestamp(str); if (timestamp < 1) { printf("Error reading timestamp from replay log file.\n"); fclose(fp); return -1; } // If the line is within range of the requested time, count it if (timestamp >= timenow - seconds) { numlines++; } } fclose(fp); return numlines; } // Set 'str' to the line in the log with a timestamp of greater than 'seconds' // seconds ago, plus however many lines 'linenum' is set to. // Also modify the line to include a timestamp in the form "[HH:MM:SS]", or [DD/MM/YY HH:MM:SS] // if settings.replaydates == 1. // Returns 1 on success, 0 on failure, or -1 if the line should be ignored. // TODO - This is horribly inefficient since it re-reads the entire file each call, rewrite this! int readreplayline(int seconds, int linenum, char *str, struct settings *settings, struct ircdstate *ircdstate) { FILE *fp; char line[MAXCHAR]; char filename[PATH_MAX]; // Build path if (!snprintf(filename, PATH_MAX, "%s/replay.log", settings->basedir)) { debugprint(DEBUG_CRIT, "readreplayline(): error: couldn't prepare replay path, exiting!\n"); exit(1); } int count = 0; fp = fopen(filename, "r"); if (fp == NULL) { debugprint(DEBUG_CRIT, "readreplayline(): could not open replay log '%s'.\n", filename); return 0; } // Get the current time for comparison later int timenow = (int)time(NULL); while (fgets(line, MAXCHAR, fp) != NULL) { // Read the timestamp from each line int timestamp = gettimestamp(line); if (timestamp < 1) { debugprint(DEBUG_CRIT, "readreplayline(): error: reading timestamp from replay log file.\n"); fclose(fp); return 0; } // If the line is within range of the requested time... if (timestamp >= timenow - seconds) { // ...and it is the current requested line... if (count == linenum) { // ...and it wasn't before blabouncer launched... if (timestamp < ircdstate->launchtime) { // Don't replay if this replay line happened before blabouncer launched, // to avoid weird synchronisation issues with uncertain events from before // we launched. debugprint(DEBUG_FULL, "readreplayline(): Ignoring line '%s' from before we launched.\n", line); fclose(fp); return -1; } // ...then return it // Insert our formatted [HH:MM:SS] timestamp into the message formattime(line, settings->replaydates); strncpy(str, line, strlen(line)); str[strlen(line)] = '\0'; fclose(fp); return 1; } count++; } } // If we got here something went wrong debugprint(DEBUG_CRIT, "readreplayline(): error: reached the end of function (seconds %d, linenum %d)\n", seconds, linenum); fclose(fp); return 0; } // Returns the line number in the replay log file on which 'nick' last spoke, or -1 if there is a problem. // 'basedir' is the directory in which to find 'replay.log'. long lastspokelinenumber(char *nick, char *basedir) { FILE *fp; char line[MAXCHAR]; char filename[PATH_MAX]; // Build path if (!snprintf(filename, PATH_MAX, "%s/replay.log", basedir)) { debugprint(DEBUG_CRIT, "lastspokelinenumber(): error: couldn't prepare replay path, exiting!\n"); exit(1); } long linenumber = -1; // Line number where 'nick' last spoke fp = fopen(filename, "r"); if (fp == NULL) { debugprint(DEBUG_FULL, "error: lastspokelinenumber(): could not open replay log '%s'.\n", filename); // Assume the file just doesn't exist yet - TODO - Interpret error codes to see what happened. return -1; } long curlinenumber = -1; // Current line number of this file reading loop while (fgets(line, MAXCHAR, fp) != NULL) { curlinenumber++; // Split the line up to determine if it was a PRIVMSG sent by the requested 'nick' // TODO - This may also be terribly inefficient // Copy to a temporary string char *strcopy = strdup(line); // Keep track of initial pointer for free()ing later char *strcopyPtr = strcopy; // Build array of each space-separated token, only need three ( :!@ PRIVMSG) char tokens[3][MAXDATASIZE + TIMELEN]; // Make IRC message length + our unix timestamp char *token; int counter = 0; for (int i = 0; i < 3; i++) { // Try to split if ((token = strsep(&strcopy, " ")) == NULL) { debugprint(DEBUG_CRIT, "lastspokelinenumber(): error splitting string on iteration '%d', returning -1!\n", i); return -1; } // Copy into the token array (strlen + 1 to get the NULL terminator) strncpy(tokens[i], token, strlen(token) + 1); counter++; } free(strcopyPtr); // Make sure there were at least three tokens if (counter < 3) { debugprint(DEBUG_CRIT, "lastspokelinenumber(): not enough tokens on line, only '%d', returning -1!\n", counter); return -1; } // Make sure it started with a valid timestamp int timestamp = gettimestamp(tokens[0]); if (timestamp < 0) { debugprint(DEBUG_CRIT, "lastspokelinenumber(): line didn't start with a timestamp, returning -1!\n", counter); } // Is it a PRIVMSG? if (strncmp(tokens[2], "PRIVMSG", strlen("PRIVMSG"))) { // Not a PRIVMSG, continue continue; } // Was it said by our 'nick'? Disable extractnickfromprefix() debugging // as it gets very noisy when we call it from here. extractnickfromprefix(tokens[1], 0); if ((strlen(tokens[1]) == strlen(nick)) && (strncmp(tokens[1], nick, strlen(nick)) == 0)) { // Our 'nick' found, set the line number linenumber = curlinenumber; } } debugprint(DEBUG_FULL, "lastspokelinenumber(): last spoke on line '%ld'.\n", linenumber); fclose(fp); return linenumber; } // Send the requested number of seconds worth of replay log lines to the requested client. // 'sourcefd' is the client to send to, and 'replayseconds' is the number of // seconds of replay log to replay. // Returns 1 for success or 0 for failure. int doreplaytime(int sourcefd, int replayseconds, struct client *clients, struct settings *settings, struct ircdstate *ircdstate, struct channel *channels) { char outgoingmsg[MAXDATASIZE]; // Figure out how many lines to replay int numlines = replaylinestime(replayseconds, settings->basedir); debugprint(DEBUG_FULL, "doreplaytime(): Replay log lines: '%d'.\n", numlines); if (numlines < 0) { debugprint(DEBUG_CRIT, "doreplaytime(): Error getting number of replay lines.\n"); return 0; } else if (numlines == 0) { snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :0 replay log lines found in the time requested, nothing to send.", ircdstate->ircnick); sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); return 1; } // Announce the start snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Starting log replay....", ircdstate->ircnick); sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); // Replay those lines! for (int i = 0; i < numlines; i++) { int ret = readreplayline(replayseconds, i, outgoingmsg, settings, ircdstate); if (ret == 0) { debugprint(DEBUG_CRIT, "doreplaytime(): Error requesting replay line.\n"); return 0; } else if (ret == -1) { debugprint(DEBUG_FULL, "doreplaytime(): readreplayline() said to ignore replay line.\n"); continue; } if (sanitisereplay(outgoingmsg, ircdstate, channels) < 1) { continue; } debugprint(DEBUG_FULL, "doreplaytime(): Sending replay line: '%s'.\n", outgoingmsg); sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); } // Announce the end snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Log replay complete.", ircdstate->ircnick); sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); return 1; } // Send replay log lines from line number 'linenumber' onwards, to client 'sourcefd'. // Returns 1 for success or 0 for failure. int doreplaylastspoke(int sourcefd, long linenumber, struct client *clients, struct settings *settings, struct ircdstate *ircdstate, struct channel *channels) { debugprint(DEBUG_FULL, "doreplaylastspoke(): replaying from linenumber '%ld' to sourcefd '%d'.\n", linenumber, sourcefd); FILE *fp; char line[MAXCHAR]; char filename[PATH_MAX]; char outgoingmsg[MAXDATASIZE]; // Build path if (!snprintf(filename, PATH_MAX, "%s/replay.log", settings->basedir)) { debugprint(DEBUG_CRIT, "doreplaylastspoke(): error: couldn't prepare replay path, exiting!\n"); exit(1); } fp = fopen(filename, "r"); if (fp == NULL) { debugprint(DEBUG_FULL, "error: doreplaylastspoke(): could not open replay log '%s'.\n", filename); // Assume the file just doesn't exist yet - TODO - Interpret error codes to see what happened. return 0; } // Announce the start snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Starting log replay....", ircdstate->ircnick); sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); long curlinenumber = -1; // Current line number of this file reading loop while (fgets(line, MAXCHAR, fp) != NULL) { curlinenumber++; // Skip through the file until line number 'linenumber'... if (curlinenumber < linenumber) { continue; } // ...carry on once we've reached 'linenumber' // Read the timestamp from the line int timestamp = gettimestamp(line); // Make sure and it wasn't before blabouncer launched... if (timestamp < ircdstate->launchtime) { // Don't replay if this replay line happened before blabouncer launched, // to avoid weird synchronisation issues with uncertain events from before // we launched. debugprint(DEBUG_FULL, "doreplaylastspoke(): Ignoring line '%s' from before we launched.\n", line); continue; } // Insert our formatted [HH:MM:SS] timestamp into the message formattime(line, settings->replaydates); strncpy(outgoingmsg, line, strlen(line)); outgoingmsg[strlen(line)] = '\0'; if (sanitisereplay(outgoingmsg, ircdstate, channels) < 1) { continue; } debugprint(DEBUG_FULL, "doreplaylastspoke(): Sending replay line: '%s'.\n", outgoingmsg); sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); } // Announce the end snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Log replay complete.", ircdstate->ircnick); sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); return 1; } // Write the line 'str' to the replay log file after prepending it with // the current unixtime timestamp. 'basedir' is the directory in which // to write to 'replay.log'. // Expects a string in the format: // :from!bar@baz PRIVMSG to :hello world // With the ":foo!bar@baz "prefix being important. // Returns 1 on success or 0 on failure. int writereplayline(char *str, char *basedir) { // Ensure "str" isn't too long if (strlen(str) >= MAXCHAR) { // String is too long! debugprint(DEBUG_CRIT, "writereplayline(): str '%s' was too long (%d out of a max of %d characters).\n", str, strlen(str), MAXCHAR - 1); return 0; } else if (strlen(str) >= MAXCHAR - 2 && str[strlen(str) - 1] != '\r' && str[strlen(str)] != '\n') { // Ensure string can fit CRLF at the end if it doesn't already have it debugprint(DEBUG_CRIT, "writereplayline(): non-CRLF message too long to append CRLF to.\n"); return 0; } FILE *fp; char line[MAXCHAR]; char filename[PATH_MAX]; // Build path snprintf(filename, PATH_MAX, "%s/replay.log", basedir); int bytes = 0; fp = fopen(filename, "a"); if (fp == NULL) { debugprint(DEBUG_CRIT, "error: could not open replay log '%s' for writing.\n", filename); return 0; } // Get the current time and manipulate it into a C string time_t timenow = time(NULL); int timenowlen = snprintf(NULL, 0, "%ld", timenow); char timenowstr[timenowlen + 1]; // TODO - Make this Year 2038 proof. snprintf(timenowstr, timenowlen + 1, "%ld", timenow); // Prepend the unixtime timestamp snprintf(line, MAXCHAR, "%s %s", timenowstr, str); // Ensure the line finishes with CRLF appendcrlf(line); debugprint(DEBUG_FULL, "Complete replay log string to write: '%s', length '%ld'.\n", line, strlen(line)); // Write complete line to file if ((bytes = fprintf(fp, "%s", line)) < 0) { debugprint(DEBUG_CRIT, "error: could not write to replay log file.\n"); fclose(fp); return 0; } fclose(fp); return bytes; }