From 2a1d4b2e958de1581e9bda7b07b705b963e394a6 Mon Sep 17 00:00:00 2001 From: Luke Bratch Date: Mon, 11 Aug 2025 23:02:08 +0100 Subject: Implement update checking using the command "BLABOUNCER UPDATECHECK", or optionally (enabled by default, toggled with configuration option "checkupdates") at startup and successful client authentication. This is implemented using a DNS TXT record check to the domain "version.blabouncer.blatech.net". --- Makefile | 2 +- README | 1 + TODO | 2 - blabouncer.c | 22 +++++++++ blabouncer.conf.example | 4 ++ functions.c | 116 ++++++++++++++++++++++++++++++++++++++++++++++++ functions.h | 13 ++++++ message.c | 54 ++++++++++++++++++++++ structures.h | 1 + 9 files changed, 212 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index baa334c..7b02278 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ ifeq ($(PREFIX),) endif blabouncer: blabouncer.c functions.c sockets.c config.c replay.c logging.c message.c config.h functions.h logging.h message.h replay.h sockets.h structures.h - $(CC) -D_DEFAULT_SOURCE -D_BSD_SOURCE -DVERSION=\"$(GIT_VERSION)\" -std=gnu99 -Wall -Wextra -lssl -lcrypto -o $(BINARY) blabouncer.c functions.c sockets.c config.c replay.c logging.c message.c + $(CC) -D_DEFAULT_SOURCE -D_BSD_SOURCE -DVERSION=\"$(GIT_VERSION)\" -std=gnu99 -Wall -Wextra -lssl -lcrypto -lresolv -o $(BINARY) blabouncer.c functions.c sockets.c config.c replay.c logging.c message.c .PHONY: clean clean: diff --git a/README b/README index e3e5c7b..d03330d 100644 --- a/README +++ b/README @@ -56,6 +56,7 @@ what this client has missed.) "BLABOUNCER DISCONNECT [FD]" (To disconnect a client with file descriptor number [FD] (see LISTCLIENTS output).) "BLABOUNCER VERSION" (To show the current blabouncer version.) +"BLABOUNCER UPDATECHECK" (To check for blabouncer updates. Set checkupdates = "0" to stop this happening automatically.) Blabouncer commands are all prefixed with BLABOUNCER which you can usually send using "/QUOTE BLABOUNCER". diff --git a/TODO b/TODO index 895e3c2..eb7bf2b 100644 --- a/TODO +++ b/TODO @@ -52,8 +52,6 @@ QUIT not logged in all channels a person was in? (e.g. Joey Mon 1 Apr 20:49:14 "/whois" with no nick - "No nickname given" goes to all clients - fixable? -Ability to check for updates (and optional at startup?). - Absurd CPU usage and duration doing "/BLABOUNCER REPLAY 24:0" approx. 14/09/2024 17:35. Custom OpenSSL protocols/ciphers? diff --git a/blabouncer.c b/blabouncer.c index b3e239d..443c75c 100644 --- a/blabouncer.c +++ b/blabouncer.c @@ -1331,6 +1331,13 @@ int main(int argc, char *argv[]) { strncat(conffailmsg, "Error getting 'alertautheddisconnect' from configuration file.\n", sizeof conffailmsg - strlen(conffailmsg) - 1); } + // Is check for updates upon startup and successful client authentication enabled? + settings.checkupdates = getconfint("checkupdates", settings.conffile); + if (errno == ECONFINT) { + conffail = 1; + strncat(conffailmsg, "Error getting 'checkupdates' from configuration file.\n", sizeof conffailmsg - strlen(conffailmsg) - 1); + } + // How many debug logs should we keep? settings.debugkeep = getconfint("debugkeep", settings.conffile); if (errno == ECONFINT) { @@ -1439,6 +1446,21 @@ int main(int argc, char *argv[]) { debugprint(DEBUG_SOME, "Using configuration file '%s'.\n", settings.conffile); + // Check for updates (if enabled in configuration file with checkupdates = "1") + if (settings.checkupdates) { + debugprint(DEBUG_SOME, "Checking for blabouncer updates (checkupdates = \"0\" to disable)...\n"); + char version[MAXDNSTXTLEN]; + version[0] = '\0'; + int ret = checkversion(version); + if (ret == 1) { + debugprint(DEBUG_SOME, "main(): blabouncer appears to be up to date.\n"); + } else if (ret == -1) { + debugprint(DEBUG_SOME, "main(): blabouncer appears to be out of date, latest version is %s.\n", version); + } else { + debugprint(DEBUG_CRIT, "main(): Version check failed!\n"); + } + } + // Unless specified otherwise on the command line, fork to background if (settings.background) { pid_t pid, sid; diff --git a/blabouncer.conf.example b/blabouncer.conf.example index 2281806..4e8e7f1 100644 --- a/blabouncer.conf.example +++ b/blabouncer.conf.example @@ -123,3 +123,7 @@ alertunautheddisconnect = "1" # Send NOTICE to all other clients upon authenticated client disconnections ("1" for yes or "0" for no) alertautheddisconnect = "1" + +# Check for updates upon startup and successful client authentication ("1" for yes or "0" for no) +# This sends a DNS TXT request to blatech.net, disable this if you do not wish for that to happen +checkupdates = "1" diff --git a/functions.c b/functions.c index 54e2663..35be8e4 100644 --- a/functions.c +++ b/functions.c @@ -1710,3 +1710,119 @@ void strlower(char *string) { string[i] = tolower(string[i]); } } + +// Gets a single TXT record from DNS for "dnsname" and stores the result in "record" +// Returns 1 on success or 0 on failure +// TODO: If this is ever used for more than just version checks, then it needs to do more than a single record. +int gettxtrecordsingle(char *dnsname, char *record) { + debugprint(DEBUG_FULL, "gettxtrecordsingle(): given '%s'.\n", dnsname); + + unsigned char buffer[8000] = {0}; + char result[MAXDNSTXTLEN] = {0}; + const unsigned char *presult = NULL; + struct __res_state resState = {0}; + ns_msg nsMsg = {0}; + ns_rr rr; + + int type = 0; + int ret = 0; + int size = 0; + int count = 0; + long unsigned int len = 0; + int res_init = 0; + + ret = res_ninit(&resState); + + if (ret) { + debugprint(DEBUG_CRIT, "gettxtrecordsingle(): res_ninit() returned failure (it returned %d)! Returning 0.\n", ret); + return 0; + } else { + res_init = 1; + } + + memset(buffer, 0, sizeof (buffer)); + size = res_nquery(&resState, dnsname, C_IN, T_TXT, buffer, sizeof(buffer) - 1); + + if (size < 1) { + res_nclose(&resState); + debugprint(DEBUG_CRIT, "gettxtrecordsingle(): res_nquery() returned zero-length or failure (it returend %d)! Returning 0.\n", size); + return 0; + } + + ret = ns_initparse(buffer, size, &nsMsg); + + if (ret) { + res_nclose(&resState); + debugprint(DEBUG_CRIT, "gettxtrecordsingle(): ns_initparse() returned failure (failed to parse the buffer buffer?) (it returned %d)! Returning 0.\n", ret); + return 0; + } + + count = ns_msg_count(nsMsg, ns_s_an); + + for (int i = 0; i < count; i++) { + ret = ns_parserr(&nsMsg, ns_s_an, i , &rr); + + if (ret) { + res_nclose(&resState); + return 0; + } + + type = ns_rr_type(rr); + if (ns_t_txt == type) { + len = ns_rr_rdlen(rr); + presult = ns_rr_rdata(rr); + + if ((len > 1) && (len < sizeof(result))) { + len--; + memcpy (result, presult+1, len); + result[len] = '\0'; + debugprint(DEBUG_FULL, "gettxtrecordsingle(): record '%d' from DNS is '%s'.\n", i, result); + + // Only using the first result + if (i == 0) { + strncpy(record, result, sizeof(result) - 1); + } + } + } + } + + debugprint(DEBUG_FULL, "gettxtrecordsingle(): using record '%s'.\n", result); + + if (res_init) { + res_nclose (&resState); + } + + return 1; +} + +// Gets the latest blabouncer version string from DNS and stores it in "version" +// Ignores the Git version portion (everything after the first '-') of VERSION, if present +// Returns 1 if local VERSION is current, -1 if local VERSION is not current, or 0 on failure +int checkversion(char *version) { + if (!gettxtrecordsingle(VERSIONTXTNAME, version)) { + debugprint(DEBUG_CRIT, "checkversion(): gettxtrecordsingle() failure and the buffer contains '%s', returning 0.\n", version); + return 0; + } else { + debugprint(DEBUG_FULL, "checkversion(): gettxtrecordsingle() success and the returned record is '%s'.\n", version); + } + + // Ignore the Git version portion (everything after the first '-') of VERSION, if present + char localversion[MAXDNSTXTLEN]; + strncpy(localversion, VERSION, sizeof(localversion) - 1); + + for (size_t i = 0; i < strlen(localversion); i++) { + if (localversion[i] == '-') { + localversion[i] = '\0'; + debugprint(DEBUG_FULL, "checkversion(): Trimmed local VERSION string to '%s'.\n", localversion); + break; + } + } + + if (strncmp(localversion, version, sizeof(localversion))) { + debugprint(DEBUG_SOME, "checkversion(): Returned version '%s' doesn't match expected '%s', returning -1.\n", version, localversion); + return -1; + } else { + debugprint(DEBUG_FULL, "checkversion(): Returned version '%s' matches expected '%s', returning 1.\n", version, localversion); + return 1; + } +} diff --git a/functions.h b/functions.h index ff9d656..72a2129 100644 --- a/functions.h +++ b/functions.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +53,8 @@ #define MAXCLIENTS 32 // Maximum number of clients that can connect to the bouncer at a time #define MAXCHANNELS 1024 // Let's assume 1024 is reasonable for now (it's configured per IRCd) #define MAXRFCNICKLEN 9 // From RFC 1459 +#define MAXDNSTXTLEN 2048 // Maximum size to store in a DNS TXT record response +#define VERSIONTXTNAME "version.blabouncer.blatech.net" // DNS name of blabouncer latest version check TXT record #define MAXTOKENS 100 // For strsep string splitting @@ -220,4 +223,14 @@ void stripprefixesfromnick(char *nick); // Convert the given 'string' into lowercase void strlower(char *string); + +// Gets a single TXT record from DNS for "dnsname" and stores the result in "record" +// Returns 1 on success or 0 on failure +// TODO: If this is ever used for more than just version checks, then it needs to do more than a single record. +int gettxtrecordsingle(char *dnsname, char *record); + +// Gets the latest blabouncer version string from DNS and stores it in "version" +// Ignores the Git version portion (everything after the first '-') of VERSION, if present +// Returns 1 if local VERSION is current, -1 if local VERSION is not current, or 0 on failure +int checkversion(char *version); #endif diff --git a/message.c b/message.c index f8e14cb..f580419 100644 --- a/message.c +++ b/message.c @@ -1033,6 +1033,31 @@ int processclientmessage(SSL *server_ssl, char *str, struct client *clients, int // Send our own greeting message snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Welcome to blabouncer version %s!", ircdstate->ircnick, VERSION); sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); + + // Check for updates (if enabled in configuration file with checkupdates = "1") + if (settings->checkupdates) { + debugprint(DEBUG_FULL, "Checking for blabouncer updates on client connection.\n"); + char version[MAXDNSTXTLEN]; + version[0] = '\0'; + int ret = checkversion(version); + if (ret == 1) { + snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Blabouncer appears to be up to date.", ircdstate->ircnick); + sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); + } else if (ret == -1) { + + // Next prepare the topic who/when message... + if (!snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Blabouncer appears to be out of date, latest version is %s.", ircdstate->ircnick, version)) { + fprintf(stderr, "processclientmessage() Error while preparing out of date version warning after client connected (version string too long?)!\n"); + debugprint(DEBUG_CRIT, "processclientmessage() Error while preparing out of date version warning after client connected (version string too long?)!\n"); + snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Blabouncer appears to be out of date, but couldn't print latest version in this NOTICE.", ircdstate->ircnick); + } + sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); + } else { + snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Version check failed!", ircdstate->ircnick); + sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); + } + } + snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Blabouncer commands are all prefixed with BLABOUNCER which you can usually send using \"/QUOTE BLABOUNCER\"", ircdstate->ircnick); sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Valid blabouncer commands are:", ircdstate->ircnick); @@ -1051,6 +1076,8 @@ int processclientmessage(SSL *server_ssl, char *str, struct client *clients, int sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :\"BLABOUNCER VERSION\" (To show the current blabouncer version.)", ircdstate->ircnick); sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); + snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :\"BLABOUNCER UPDATECHECK\" (To check for blabouncer version updates.)", ircdstate->ircnick); + sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); // Get the channel count so we can iterate over all channels. int channelcount = getchannelcount(channels, ircdstate->maxchannelcount); @@ -1527,6 +1554,31 @@ int processclientmessage(SSL *server_ssl, char *str, struct client *clients, int debugprint(DEBUG_SOME, "Client BLABOUNCER VERSION found and it is: %s with length %zd!\n", tokens[1], strlen(tokens[1])); snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :This is blabouncer version %s!", ircdstate->ircnick, VERSION); sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); + return 1; + // UPDATECHECK received, send current blabouncer version + } else if (strncasecmp(tokens[1], "UPDATECHECK", strlen("UPDATECHECK")) == 0) { + debugprint(DEBUG_SOME, "Client BLABOUNCER UPDATECHECK found and it is: %s with length %zd!\n", tokens[1], strlen(tokens[1])); + + char version[MAXDNSTXTLEN]; + version[0] = '\0'; + int ret = checkversion(version); + if (ret == 1) { + snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Blabouncer appears to be up to date.", ircdstate->ircnick); + sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); + } else if (ret == -1) { + + // Next prepare the topic who/when message... + if (!snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Blabouncer appears to be out of date, latest version is %s.", ircdstate->ircnick, version)) { + fprintf(stderr, "processclientmessage() Error while preparing out of date version warning after client connected (version string too long?)!\n"); + debugprint(DEBUG_CRIT, "processclientmessage() Error while preparing out of date version warning after client connected (version string too long?)!\n"); + snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Blabouncer appears to be out of date, but couldn't print latest version in this NOTICE.", ircdstate->ircnick); + } + sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); + } else { + snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Version check failed!", ircdstate->ircnick); + sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); + } + return 1; // LISTCLIENTS received, send list of connected clients and their authentication status } else if (strncasecmp(tokens[1], "LISTCLIENTS", strlen("LISTCLIENTS")) == 0) { @@ -1610,6 +1662,8 @@ int processclientmessage(SSL *server_ssl, char *str, struct client *clients, int sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :\"BLABOUNCER VERSION\" (To show the current blabouncer version.)", ircdstate->ircnick); sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); + snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :\"BLABOUNCER UPDATECHECK\" (To check for blabouncer version updates.)", ircdstate->ircnick); + sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); return 1; } } diff --git a/structures.h b/structures.h index c4d20e0..86f83b7 100644 --- a/structures.h +++ b/structures.h @@ -90,6 +90,7 @@ struct settings { int alertauthsuccess; int alertunautheddisconnect; int alertautheddisconnect; + int checkupdates; }; // Structure of a connected client, their socket/file descriptors, their authentication status, and their OpenSSL structures -- cgit v1.2.3