From 0c153207be8a31fc66b7f6e9d0b849357017e34a Mon Sep 17 00:00:00 2001 From: Luke Bratch Date: Sat, 1 Jun 2019 17:54:11 +0100 Subject: Handle SIGINT (Ctrl-C) and SIGTERM (kill command) cleanly and fix unrelated server reconnect upon send() returning -1 issue. --- TODO | 2 +- blabouncer.c | 92 ++++++++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 75 insertions(+), 19 deletions(-) diff --git a/TODO b/TODO index 81e62bc..2b3b030 100644 --- a/TODO +++ b/TODO @@ -9,4 +9,4 @@ Might need to #include in blabouncer.c to make some operating systems Send a PING to the server before assuming a timeout is definite. -Capture Ctrl-C/kill and exit cleanly. +Put debug logs in subdirectory and have max number to keep be configurable. diff --git a/blabouncer.c b/blabouncer.c index cd50a0a..02c62ad 100644 --- a/blabouncer.c +++ b/blabouncer.c @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -38,11 +39,14 @@ #define DEBUG_SOME 1 #define DEBUG_FULL 2 +#define STDIN 0 // stdin is fd 0 +#define SIGINT 2 // SIGINT is signal 2 +#define SIGTERM 15 // SIGTERM is signal 15 + // It seems to be that *message length* is max 512 bytes, but a socket read from at least UnrealIRCd seems to be up to at least 2416 (+1 for null) bytes. // 1208 bytes with OpenSSL, 2416 bytes with plain text. #define MAXRCVSIZE 2417 #define MAXDATASIZE 513 // max number of bytes we can get at once (RFC2812 says 512, plus one for null terminator) -#define STDIN 0 // stdin is fd 0 #define MAXCLIENTS 32 // maximum number of clients that can connect to the bouncer at a time #define MAXTOKENS 100 // maximum number of (CRLF or space) separated tokens per server response we expect (TODO - check this is reasonable) (CRLF and spaces just grouped here due to laziness) #define MAXPONGSIZE 32 // let's assume PING/PONG responses can't be larger than this (TODO - check this [it's totally made up]!) @@ -62,6 +66,9 @@ int debug = 0; // Debug verbosity ("0" for critical only, "1" for some extra info, "2" for full debug mode) char debugpath[PATH_MAX]; // Path to debug file +// Set if certain signal (just SIGINT and SIGKILL at time of writing) is received +int signum = 0; + struct channel { char name[MAXCHANLENGTH]; char topic[MAXDATASIZE]; // TODO - Is there a particular maximum topic length? @@ -133,6 +140,12 @@ struct client { int pendingcap; // Whether the client is still negotiating IRCv3 CAPabilities. 0 = no, 1 = yes, -1 = just finished (so register them as if they had just sent USER). }; +// Signal handler +// We don't actually do anything in here, the main pselect() notice signals +void sighandler(int sig) { + signum = sig; +} + // Return index of requested client FD within the clients array. // TODO - Use this wherever we are calculating the position (various places) instead of // duplicating code. @@ -762,6 +775,30 @@ int connecttoircserver(SSL_CTX **serverctx, SSL **server_ssl, int *serversockfd, return 1; // TODO - Return 0 if this fails and make callers do something with that } +// Exit the program cleanly - tell clients, tell the server, then exit(0) +// Optional quit message string "quitmsg" +// "sourcefd" of 0 means the request didn't come from a client +void cleanexit(SSL *server_ssl, struct client *clients, int sourcefd, struct ircdstrings *ircdstrings, struct settings *settings, char *quitmsg) { + char outgoingmsg[MAXDATASIZE]; + + // Tell clients and debug log + if (sourcefd) { + snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Exiting on request from client with fd '%d', message '%s'.", ircdstrings->ircnick, sourcefd, quitmsg); + debugprint(DEBUG_CRIT, "Exiting on request from client with fd '%d', message '%s'.\n", sourcefd, quitmsg); + } else { + snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Exiting on request (not from a client), message '%s'.", ircdstrings->ircnick, quitmsg); + debugprint(DEBUG_CRIT, "Exiting on request (not from a client), message '%s'.\n", quitmsg); + } + sendtoallclients(clients, outgoingmsg, EXCEPT_NONE, settings); + + // Tell the server + // Combine "QUIT :" with any (optional) quit message + snprintf(outgoingmsg, MAXDATASIZE, "QUIT :%s", quitmsg); + sendtoserver(server_ssl, outgoingmsg, strlen(outgoingmsg), sourcefd, clients, settings); + + exit(0); +} + // Figure out what to do with each CRLF-split IRC message (if anything) // by splitting out the different components by space character (ASCII 0x20). // @@ -2078,20 +2115,14 @@ int processircmessage(SSL *server_ssl, char *str, int source, struct client *cli // QUIT received, send QUIT message to server and exit cleanly } else if (strncasecmp(tokens[1], "QUIT", strlen("QUIT")) == 0) { debugprint(DEBUG_SOME, "Client BLABOUNCER QUIT found and it is: %s with length %zd!\n", tokens[1], strlen(tokens[1])); - snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :Exiting on BLABOUNCER QUIT command from client with fd '%d'.", ircdstrings->ircnick, sourcefd); - sendtoallclients(clients, outgoingmsg, EXCEPT_NONE, settings); // Combine "QUIT :" with any optional quit message the user provided - char quitmsg[MAXDATASIZE]; if (counter > 2) { // Any optional quit message comes 16 characters after the start of the string ("BLABOUNCER QUIT " is 16 characters) - snprintf(quitmsg, MAXDATASIZE, "QUIT :%s", str + 16); + cleanexit(server_ssl, clients, sourcefd, ircdstrings, settings, str + 16); } else { - snprintf(quitmsg, MAXDATASIZE, "QUIT"); + cleanexit(server_ssl, clients, sourcefd, ircdstrings, settings, ""); } - sendtoserver(server_ssl, quitmsg, strlen(quitmsg), sourcefd, clients, settings); - debugprint(DEBUG_CRIT, "Exiting on client request.\n"); - free(strcopyPtr); - exit(0); + // Unrecognised BLABOUNCER command received, send some help instructions } else { debugprint(DEBUG_SOME, "Client BLABOUNCER unrecognised command found and it is: %s with length %zd! Sending a help message.\n", tokens[1], strlen(tokens[1])); @@ -2253,7 +2284,7 @@ void dochat(int *serversockfd, int *clientsockfd, struct settings *settings) { fdmax = *clientsockfd; // keep track of highest fd number, currently client socket as created last (after server socket) - fd_set rfds; // set of read fds to monitor with select() - 0: stdin, 1: stdout, 2: stderr, 3 and higher: sockets (at time of writing, 3: real IRC server, 4: client listener, 5 and higher: clients) + fd_set rfds; // set of read fds to monitor with pselect() - 0: stdin, 1: stdout, 2: stderr, 3 and higher: sockets (at time of writing, 3: real IRC server, 4: client listener, 5 and higher: clients) // Set up clients structure struct client clients[MAXCLIENTS]; @@ -2307,6 +2338,18 @@ void dochat(int *serversockfd, int *clientsockfd, struct settings *settings) { configure_openssl_context(ctx, settings->certfile, settings->keyfile); } + // Let's set up signal handling stuff here since we're about to enter The Big Loop (TM) + // We'll handle SIGINT (Ctrl-C) and SIGTERM (default signal of `kill`) + signal(SIGINT, sighandler); // SIGINT (2) + signal(SIGTERM, sighandler); // SIGTERM (15) + + // Block those two signals + sigset_t sigset, oldset; + sigemptyset(&sigset); + sigaddset(&sigset, SIGINT); + sigaddset(&sigset, SIGTERM); + sigprocmask(SIG_BLOCK, &sigset, &oldset); + while (1) { debugprint(DEBUG_FULL, "top of loop, fdmax %d.\n", fdmax); FD_ZERO(&rfds); // clear entries from fd set @@ -2355,12 +2398,26 @@ void dochat(int *serversockfd, int *clientsockfd, struct settings *settings) { continue; } - debugprint(DEBUG_FULL, "select()ing...\n"); - // check to see if anything in the fd_set is waiting - waits here until one of the fds in the set does something - if (select(fdmax + 1, &rfds, NULL, NULL, NULL) < 0) { // network socket + 1, rfds, no writes, no exceptions/errors, no timeout - perror("select"); - debugprint(DEBUG_CRIT, "select() error, errno '%d'.\n", errno); - continue; + debugprint(DEBUG_FULL, "pselect()ing...\n"); + // Check to see if any fd in the fd_set is waiting or a signal happened - blocks here until one one of those things happens + // (pselect() to do signal handling in additiona to fd monitoring) + if (pselect(fdmax + 1, &rfds, NULL, NULL, NULL, &oldset) < 0) { // network socket + 1, rfds, no writes, no exceptions/errors, no timeout, original sigmask + if (errno == EINTR) { + // Signal caught, do signal handling + debugprint(DEBUG_CRIT, "signal '%d' happened, exiting!\n", signum); + if (signum == SIGINT) { + cleanexit(server_ssl, clients, 0, &ircdstrings, settings, "SIGINT received"); + } else if (signum == SIGTERM) { + cleanexit(server_ssl, clients, 0, &ircdstrings, settings, "SIGTERM received"); + } else { + cleanexit(server_ssl, clients, 0, &ircdstrings, settings, "Unexpected signal received"); + } + } else { + // Some other error + perror("pselect"); + debugprint(DEBUG_CRIT, "pselect() error, errno '%d'.\n", errno); + continue; + } } // TODO - switch around the serversockfd and STDIN FD_ISSET if-statements? They feel the wrong way round. Are they like this on purpose? I can't remember. @@ -2374,7 +2431,6 @@ void dochat(int *serversockfd, int *clientsockfd, struct settings *settings) { perror("recv"); printf("receive error (-1), skipping loop iteration...\n"); debugprint(DEBUG_CRIT, "serversockfd receive error (-1), skipping loop iteration, errno '%d'.\n"); - continue; } else if (servernumbytes == 0) { perror("recv"); printf("socket closed (or no data received) (0), skipping loop iteration...\n"); -- cgit v1.2.3