From 871184560b17553e84cc711a487ddf928671c56c Mon Sep 17 00:00:00 2001 From: Luke Bratch Date: Mon, 27 May 2019 21:53:47 +0100 Subject: Rewrite all server connection code to enable reconnecting to the server if there was a timeout, an error, or if the server disconnected us. --- TODO | 4 +- blabouncer.c | 274 +++++++++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 203 insertions(+), 75 deletions(-) diff --git a/TODO b/TODO index 3b714ee..6ca0a72 100644 --- a/TODO +++ b/TODO @@ -9,4 +9,6 @@ Add various auto replay options: Might need to #include in blabouncer.c to make some operating systems and/or compilers happy. -Reconnect server if we get disconnected for some reason. +Send a PING to the server before assuming a timeout is definite. + +Include server 671 responses in WHO pending code. diff --git a/blabouncer.c b/blabouncer.c index a180b5a..6149ef1 100644 --- a/blabouncer.c +++ b/blabouncer.c @@ -1,6 +1,5 @@ // TODO: // - Perhaps rename clients.ssl and server_ssl since they may not even be OpenSSL sockets -// - Move debug output into some debug function // "server" means the real IRC server // "client" means bouncer clients @@ -52,6 +51,7 @@ #define MAXREALNAMELEN 128 // Randomly picked (TODO - is there an actual maximum real name length?) #define MAXPORTLEN 6 // Up to 65535, so 5 characters + 1 for null #define MAXAUTOCHANLEN 1024 // Randomly picked maximum length of the auto channel list +#define SERVERTIMEOUT 300 // How many seconds to wait without hearing from the server before assuming a timeout struct channel { char name[MAXCHANLENGTH]; @@ -63,6 +63,7 @@ struct channel { int gotnames; // Have we finished getting the RPL_NAMREPLYs for this channel yet? }; +// TODO - Rename this or split into multiple structs since it's no longer strictly just IRCd strings struct ircdstrings { char greeting001[MAXDATASIZE]; char greeting002[MAXDATASIZE]; @@ -79,6 +80,9 @@ struct ircdstrings { char mode[MAXDATASIZE]; int capmultiprefix; // Whether the server approved our CAP multi-prefix request int autonicknum; // Number of attempts made at automatically setting a nick if all configured nicks were in use + int lastmessagetime; // The last time we heard from the server + int reconnecting; // Whether or not we're reconnecting due to an earlier disconnection + char oldnick[MAXNICKLENGTH]; // Set temporarily if we end up reconnecting in case we need to tell existing clients about a nick change }; // Structure of settings either to be read from the configuration file or set/changed at runtime @@ -670,6 +674,77 @@ void tryautonick(struct ircdstrings *ircdstrings) { printf("tryautonick(): set irdstrings->ircnick to '%s'.\n", ircdstrings->ircnick); } +int connecttoircserver(SSL_CTX **serverctx, SSL **server_ssl, int *serversockfd, struct ircdstrings *ircdstrings, struct settings *settings, struct client *clients) { + char outgoingmsg[MAXDATASIZE]; // String to send to server + + if (settings->servertls) { + printf("server openssl start.\n"); + *serverctx = create_openssl_context(SOURCE_SERVER); + configure_openssl_context(*serverctx, NULL, NULL); + *server_ssl = SSL_new(*serverctx); + SSL_set_fd(*server_ssl, *serversockfd); + if (SSL_connect(*server_ssl) == -1) { + ERR_print_errors_fp(stderr); + } else { + printf("SSL_connect() success.\n"); + } + printf("server openssl complete.\n"); + } else { + // If not using TLS then just slap the serversockfd into server_ssl by casting it + *server_ssl = (SSL*)(long int)*serversockfd; + } + + // <============================================= + // Initialise IRC connecting/registration state + + // Set ircdstrings to zero-length strings for now + ircdstrings->greeting001[0] = '\0'; + ircdstrings->greeting002[0] = '\0'; + ircdstrings->greeting003[0] = '\0'; + ircdstrings->greeting004[0] = '\0'; + ircdstrings->greeting005a[0] = '\0'; + ircdstrings->greeting005b[0] = '\0'; + ircdstrings->greeting005c[0] = '\0'; + ircdstrings->ircdname[0] = '\0'; + ircdstrings->nickuserhost[0] = '\0'; + ircdstrings->ircnick[0] = '\0'; + ircdstrings->ircusername[0] = '\0'; + ircdstrings->currentmsg[0] = '\0'; + ircdstrings->mode[0] = '\0'; + // ircdstrings->oldnick is not set here as we want to track reconnections separately + // And set non-string things to zero (TODO - Rename this from ircdstrings since it's not all strings any more) + ircdstrings->capmultiprefix = 0; + ircdstrings->autonicknum = 0; + ircdstrings->lastmessagetime = time(NULL); + // ircdstrings.reconnecting is not set here as we want to track reconnections separately + + // Populate nick and username from our configuration file for now, real IRCd may change them later (TODO - Is this true of username?) + strcpy(ircdstrings->ircnick, settings->ircnick); + strcpy(ircdstrings->ircusername, settings->ircusername); + + // Send the server password if one was configured + if (settings->ircserverpassword[0]) { + snprintf(outgoingmsg, MAXDATASIZE, "PASS %s", settings->ircserverpassword); + // sourcefd = 0 as this is a trusted message + sendtoserver(*server_ssl, outgoingmsg, strlen(outgoingmsg), 0, clients, settings); + } + + // Send our NICK + snprintf(outgoingmsg, MAXDATASIZE, "NICK %s", ircdstrings->ircnick); // TODO - Check for success (with return code) + // sourcefd = 0 as this is a trusted message + sendtoserver(*server_ssl, outgoingmsg, strlen(outgoingmsg), 0, clients, settings); + + // Send our USER + snprintf(outgoingmsg, MAXDATASIZE, "USER %s 8 * : %s", ircdstrings->ircusername, settings->ircrealname); // TODO - Check for success (with return code) + // TODO - Send a more intelligent/correct USER string + // sourcefd = 0 as this is a trusted message + sendtoserver(*server_ssl, outgoingmsg, strlen(outgoingmsg), 0, clients, settings); + + // =============================================> + + return 1; // TODO - Return 0 if this fails and make callers do something with that +} + // Figure out what to do with each CRLF-split IRC message (if anything) // by splitting out the different components by space character (ASCII 0x20). // @@ -710,10 +785,13 @@ int processircmessage(SSL *server_ssl, char *str, int source, struct client *cli } // <============================================= - // IRC command processing (commands from server) + // IRC command processing (commands from server or client) switch(source) { case SOURCE_SERVER: // If message(s) were from the real IRC server + // Record that we received something from the server for timeout checking purposes + ircdstrings->lastmessagetime = time(NULL); // snprintf(NULL, 0, "%ld", timenow); + // Server PING received? If so, send a PONG back with the next element as the argument. if (strncmp(tokens[0], "PING", strlen(tokens[0])) == 0) { printf("Server PING found and it is: %s with length %zd! Sending response...\n", tokens[0], strlen(tokens[0])); @@ -779,8 +857,53 @@ int processircmessage(SSL *server_ssl, char *str, int source, struct client *cli // Receiving greeting 004 means we're now registered // Request IRCv3 multi-prefix extension so we can more accurately inform new clients about current user prefixes sendtoserver(server_ssl, "CAP REQ multi-prefix", strlen("CAP REQ multi-prefix"), 0, clients, settings); - // Join any auto channels set in the configuration file - joinautochannels(server_ssl, clients, settings); + // If this is a reconnection, JOIN existing channels and catch clients up again + if (ircdstrings->reconnecting) { + // First tell clients if our nick changed + if (!strcmp(ircdstrings->ircnick, ircdstrings->oldnick) == 0) { + printf("Telling clients about nick change.\n"); + char nickmsg[MAXDATASIZE]; + snprintf(nickmsg, MAXDATASIZE, ":%s NICK :%s", ircdstrings->oldnick, ircdstrings->ircnick); + sendtoallclients(clients, nickmsg, sourcefd, settings); + } + + // Next re-join channels + // Storing separately so we can skip over blank channels. + int channelcount = getchannelcount(channels); + // Join all the channels and make a note of the channel count + for (int i = 0; i < channelcount; i++) { + // Skip this one and increment channelcount if it's a blank channel + if (!channels[i].name[0]) { + printf("Reconnection: Skipping channel[%d], incrementing channelcount.\n", i); + channelcount++; + continue; + } + + printf("Reconnection: Re-joining '%s'.\n", channels[i].name); + + char joinmsg[MAXDATASIZE]; + snprintf(joinmsg, MAXDATASIZE, "JOIN %s", channels[i].name); + sendtoserver(server_ssl, joinmsg, strlen(joinmsg), 0, clients, settings); + } + + // Finally do a replay for all clients and tell them we're reconnected + for (int i = 0; i < MAXCLIENTS; i++) { + if (clients[i].fd) { + doreplay(clients[i].fd, settings->replayseconds, clients, settings, ircdstrings, channels); + char alertmsg[MAXDATASIZE]; + snprintf(alertmsg, MAXDATASIZE, "NOTICE %s :Reconnection complete.", ircdstrings->ircnick); + sendtoclient(clients[i].fd, alertmsg, clients, settings, 0); + } + } + + // Reconnection complete + ircdstrings->oldnick[0] = '\0'; + ircdstrings->reconnecting = 0; + // If it's not, deal with auto channels + } else { + // Join any auto channels set in the configuration file + joinautochannels(server_ssl, clients, settings); + } free(strcopyPtr); return 1; } else if (strncmp(tokens[1], "005", strlen(tokens[1])) == 0) { @@ -1449,6 +1572,7 @@ int processircmessage(SSL *server_ssl, char *str, int source, struct client *cli if (!strlen(ircdstrings->greeting004)) { sendtoclient(sourcefd, "Sorry, we aren't registered with a real IRC server yet.", clients, settings, 0); disconnectclient(sourcefd, clients, ircdstrings, settings); + free(strcopyPtr); return 1; } @@ -1978,8 +2102,6 @@ void dochat(int *serversockfd, int *clientsockfd, struct settings *settings) { char serverbuf[MAXRCVSIZE]; // buffer for receiving data on server socket char clientbuf[MAXRCVSIZE]; // buffer for receiving data on client socket(s) int servernumbytes; // Number of bytes received from remote server - char outgoingmsg[MAXDATASIZE]; // String to send to server - int outgoingmsgrc; // Return code from getstdin() for outgoing message int fdmax; // highest numbered socket fd @@ -2011,74 +2133,9 @@ void dochat(int *serversockfd, int *clientsockfd, struct settings *settings) { clients[i].pendingcap = 0; } - // Initialise OpenSSL (used for both client and server) - init_openssl(); - - // OpenSSL for server side if configured - SSL *server_ssl; // Need to create this either way as referenced later - if (settings->servertls) { - printf("server openssl start.\n"); - SSL_CTX *serverctx; - serverctx = create_openssl_context(SOURCE_SERVER); - configure_openssl_context(serverctx, NULL, NULL); - server_ssl = SSL_new(serverctx); - SSL_set_fd(server_ssl, *serversockfd); - if (SSL_connect(server_ssl) == -1) { - ERR_print_errors_fp(stderr); - } else { - printf("SSL_connect() success.\n"); - } - printf("server openssl complete.\n"); - } else { - // If not using TLS then just slap the serversockfd into server_ssl by casting it - server_ssl = (SSL*)(long int)*serversockfd; - } - - // <============================================= - // Initialise IRC connecting/registration state - // Struct of various strings from and for the real IRCd (such as the greeting strings, the real IRCd's name, // our nick!user@host string, our nick, username, real name, etc.) struct ircdstrings ircdstrings; - // Set them to zero-length strings for now - ircdstrings.greeting001[0] = '\0'; - ircdstrings.greeting002[0] = '\0'; - ircdstrings.greeting003[0] = '\0'; - ircdstrings.greeting004[0] = '\0'; - ircdstrings.greeting005a[0] = '\0'; - ircdstrings.greeting005b[0] = '\0'; - ircdstrings.greeting005c[0] = '\0'; - ircdstrings.ircdname[0] = '\0'; - ircdstrings.nickuserhost[0] = '\0'; - ircdstrings.ircnick[0] = '\0'; - ircdstrings.ircusername[0] = '\0'; - ircdstrings.currentmsg[0] = '\0'; - ircdstrings.mode[0] = '\0'; - // And set non-string things to zero (TODO - Rename this from ircdstrings since it's not all strings any more) - ircdstrings.capmultiprefix = 0; - ircdstrings.autonicknum = 0; - - // Populate nick and username from our configuration file for now, real IRCd may change them later (TODO - Is this true of username?) - strcpy(ircdstrings.ircnick, settings->ircnick); - strcpy(ircdstrings.ircusername, settings->ircusername); - - // Send the server password if one was configured - if (settings->ircserverpassword[0]) { - snprintf(outgoingmsg, MAXDATASIZE, "PASS %s", settings->ircserverpassword); - // sourcefd = 0 as this is a trusted message - sendtoserver(server_ssl, outgoingmsg, strlen(outgoingmsg), 0, clients, settings); - } - - // Send our NICK - snprintf(outgoingmsg, MAXDATASIZE, "NICK %s", ircdstrings.ircnick); // TODO - Check for success (with return code) - // sourcefd = 0 as this is a trusted message - sendtoserver(server_ssl, outgoingmsg, strlen(outgoingmsg), 0, clients, settings); - - // Send our USER - snprintf(outgoingmsg, MAXDATASIZE, "USER %s 8 * : %s", ircdstrings.ircusername, settings->ircrealname); // TODO - Check for success (with return code) - // TODO - Send a more intelligent/correct USER string - // sourcefd = 0 as this is a trusted message - sendtoserver(server_ssl, outgoingmsg, strlen(outgoingmsg), 0, clients, settings); // Struct of channels we're in struct channel *channels; @@ -2087,7 +2144,18 @@ void dochat(int *serversockfd, int *clientsockfd, struct settings *settings) { for (int i = 0; i < MAXCHANNELS; i++) { channels[i].name[0] = '\0'; } - // =============================================> + + // Initialise OpenSSL (used for both client and server) + init_openssl(); + + // OpenSSL for server side if configured + SSL_CTX *serverctx = NULL; + SSL *server_ssl = NULL; // Need to create this either way as referenced later + + // Set reconnection things to null/zero for now (not used unless reconnecting to server) + ircdstrings.oldnick[0] = '\0'; + ircdstrings.reconnecting = 0; + connecttoircserver(&serverctx, &server_ssl, serversockfd, &ircdstrings, settings, clients); // OpenSSL context for client side (that clients connect to) (need to create this whether or not using TLS as it is referenced later) SSL_CTX *ctx; @@ -2117,6 +2185,33 @@ void dochat(int *serversockfd, int *clientsockfd, struct settings *settings) { } } + // Before we wait for a message, let's make sure the server is still responding + // TODO - Code duplication, make a function and share with socket error code below + if (ircdstrings.lastmessagetime < time(NULL) - SERVERTIMEOUT) { + printf("Server has timed out (%ld seconds), reconnecting!\n", time(NULL) - ircdstrings.lastmessagetime); + // Tell all clients if we timed out + char alertmsg[MAXDATASIZE]; + snprintf(alertmsg, MAXDATASIZE, "NOTICE %s :Server has timed out (%ld seconds), reconnecting!", ircdstrings.ircnick, time(NULL) - ircdstrings.lastmessagetime); + sendtoallclients(clients, alertmsg, 0, settings); + if (settings->servertls) { + // Finish up with OpenSSL if using server TLS + SSL_free(server_ssl); + } + // Close the socket + close(*serversockfd); + // Make a new one + *serversockfd = createserversocket(settings->ircserver, settings->ircserverport); + + // Set reconnection marker for other functions to know we're reconnecting + ircdstrings.reconnecting = 1; + // Set oldnick in case we change nick when reconnecting so we can inform existing clients + strcpy(ircdstrings.oldnick, ircdstrings.ircnick); + connecttoircserver(&serverctx, &server_ssl, serversockfd, &ircdstrings, settings, clients); + + // Back to top of loop + continue; + } + printf("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 @@ -2134,12 +2229,38 @@ void dochat(int *serversockfd, int *clientsockfd, struct settings *settings) { if ((servernumbytes = sockread(server_ssl, serverbuf, MAXRCVSIZE - 1, settings->servertls)) == -1) { printf("receive error (-1), exiting...\n"); perror("recv"); - exit(1); } else if (servernumbytes == 0) { printf("socket closed (or no data received) (0), exiting...\n"); perror("recv"); - exit(1); } + + // If there was a socket error (receive error or socket closed) + // TODO - Code duplication, make a function and share with timeout code above + if (servernumbytes < 1) { + printf("Server socket had an error (sockread return code %d), reconnecting!\n", servernumbytes); + // Tell all clients if we timed out + char alertmsg[MAXDATASIZE]; + snprintf(alertmsg, MAXDATASIZE, "NOTICE %s :Server socket had an error (sockread return code %d), reconnecting!", ircdstrings.ircnick, servernumbytes); + sendtoallclients(clients, alertmsg, 0, settings); + if (settings->servertls) { + // Finish up with OpenSSL if using server TLS + SSL_free(server_ssl); + } + // Close the socket + close(*serversockfd); + // Make a new one + *serversockfd = createserversocket(settings->ircserver, settings->ircserverport); + + // Set reconnection marker for other functions to know we're reconnecting + ircdstrings.reconnecting = 1; + // Set oldnick in case we change nick when reconnecting so we can inform existing clients + strcpy(ircdstrings.oldnick, ircdstrings.ircnick); + connecttoircserver(&serverctx, &server_ssl, serversockfd, &ircdstrings, settings, clients); + + // Back to top of loop + continue; + } + serverbuf[servernumbytes] = '\0'; printf("BOUNCER-SERVER RECEIVED: '%s', length '%d'.\n", serverbuf, servernumbytes); @@ -2156,6 +2277,9 @@ void dochat(int *serversockfd, int *clientsockfd, struct settings *settings) { if (FD_ISSET(STDIN, &rfds)) { printf("reading stdin!\n"); + char outgoingmsg[MAXDATASIZE]; // String to send to server + int outgoingmsgrc; // Return code from getstdin() for outgoing message + outgoingmsgrc = getstdin(NULL, outgoingmsg, sizeof(outgoingmsg)); if (outgoingmsgrc == NO_INPUT) { @@ -2293,6 +2417,8 @@ void dochat(int *serversockfd, int *clientsockfd, struct settings *settings) { } } } + + free(channels); } int main(int argc, char *argv[]) { -- cgit v1.2.3