/* * 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 . */ // TODO: // - Perhaps rename clients.ssl and server_ssl since they may not even be OpenSSL sockets // "server" means the real IRC server // "client" means bouncer clients #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "functions.h" #include "sockets.h" #include "config.h" #include "replay.h" #include "logging.h" #include "structures.h" #include "message.h" #define SOURCE_SERVER 0 #define SOURCE_CLIENT 1 #define EXCEPT_NONE 0 #define LOG_PRIVMSG 0 #define LOG_JOINPART 1 #define LOG_TOPIC 2 #define DEBUG_CRIT 0 #define DEBUG_SOME 1 #define DEBUG_FULL 2 #define ECONFINT 1 // errno value if getconfint() failed #define STDIN 0 // stdin is fd 0 // Various important limits - note that several related ones are defined in functions.h and structures.h // 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 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 SERVERTIMEOUT 300 // How many seconds to wait without hearing from the server before assuming a timeout #define SELECTTIMEOUT 60 // How many seconds to wait before our pselect() times out, useful for detecting server timeouts // Global debug control 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; // Signal handler // We don't actually do anything in here, the main pselect() notice signals void sighandler(int sig) { signum = sig; } int connecttoircserver(SSL_CTX **serverctx, SSL **server_ssl, int *serversockfd, struct ircdstate *ircdstate, struct settings *settings, struct client *clients) { char outgoingmsg[MAXDATASIZE]; // String to send to server if (settings->servertls) { debugprint(DEBUG_FULL, "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 { debugprint(DEBUG_FULL, "SSL_connect() success.\n"); } debugprint(DEBUG_FULL, "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 ircdstate to zero-length strings for now ircdstate->greeting001[0] = '\0'; ircdstate->greeting002[0] = '\0'; ircdstate->greeting003[0] = '\0'; ircdstate->greeting004[0] = '\0'; ircdstate->greeting005a[0] = '\0'; ircdstate->greeting005b[0] = '\0'; ircdstate->greeting005c[0] = '\0'; ircdstate->ircdname[0] = '\0'; ircdstate->nickuserhost[0] = '\0'; ircdstate->ircnick[0] = '\0'; ircdstate->ircusername[0] = '\0'; ircdstate->currentmsg[0] = '\0'; ircdstate->mode[0] = '\0'; // ircdstate->oldnick is not set here as we want to track reconnections separately // And set non-string things to zero ircdstate->capmultiprefix = 0; ircdstate->autonicknum = 0; ircdstate->lastmessagetime = time(NULL); ircdstate->timeoutcheck = 0; // ircdstate.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(ircdstate->ircnick, settings->ircnick); strcpy(ircdstate->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", ircdstate->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", ircdstate->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). // // serversockfd, clientsockfd, fdmax, arr_clients, arr_authed, all passed to here so we can // send/relay a response to server or (other) client(s) directly from this function if // we want to. // // str is the raw string // // source is either 0 (SOURCE_SERVER) (server - from the real IRC server) or 1 (SOURCE_CLIENT) (client - from a real IRC client) // // Return 1 if we processed something and expect the caller to not need to do anything more // Return 0 if we didn't process it and the caller might want to do something //int processircmessage(int *serversockfd, int *clientsockfd, char *str, int source) { int processircmessage(SSL *server_ssl, char *str, int source, struct client *clients, int sourcefd, struct ircdstate *ircdstate, struct channel *channels, struct settings *settings) { // Track which space-separated token within this response we're on int counter = 0; // Build array of each space-separated token (TODO - Use counter to stop splitting once we reach some reasonable value - i.e. once we're definitely past commands and into just free text) char tokens[MAXTOKENS][MAXDATASIZE]; debugprint(DEBUG_FULL, " >> processircmessage(): Processing source %d message \"%s\"...\n", source, str); // Copy to a temporary string so we still have the original in case it's not processed char *strcopy = strdup(str); // Keep track of initial pointer for free()ing later char *strcopyPtr = strcopy; char *token; while ((token = strsep(&strcopy, " ")) != NULL) { if (*token == '\0') continue; // Skip consecutive matches if (counter >= MAXTOKENS) break; // Too many tokens debugprint(DEBUG_FULL, " >> Message Token: \"%s\", length %zd.\n", token, strlen(token)); // Copy into the token array (strlen + 1 to get the NULL terminator) strncpy(tokens[counter], token, strlen(token) + 1); counter++; } // <============================================= // IRC command processing (commands from server or client) switch(source) { case SOURCE_SERVER: // If message(s) were from the real IRC server if (processservermessage(server_ssl, str, clients, sourcefd, ircdstate, channels, settings, tokens, counter)) { // We processed something so return true free(strcopyPtr); return 1; } // Don't return if we got here because this means we didn't process something in processservermessage() break; case SOURCE_CLIENT: // If message(s) were from a real IRC client if (processclientmessage(server_ssl, str, clients, sourcefd, ircdstate, channels, settings, tokens, counter)) { // We processed something so return true free(strcopyPtr); return 1; } // Don't return if we got here because this means we didn't process something in processclientmessage() break; default: fprintf(stderr, "Unexpected raw IRC string source!\n"); debugprint(DEBUG_CRIT, "Unexpected raw IRC string source!\n"); free(strcopyPtr); return 0; } // =============================================> debugprint(DEBUG_FULL, "Done with processircmessage(), didn't process anything.\n"); // If we got here then we didn't process anything free(strcopyPtr); return 0; } // Figure out what the client or server sent and decide what to do with it (if anything) // First by splitting out the different messages by CRLF, process if we know how to, // or send on to all other clients (if from the server) or to the server (if from a client) // if we don't know how to. // // serversockfd, clientsockfd, fdmax, arr_clients, arr_authed all passed to here so we can // send/relay a response to server or (other) client(s) for things we don't know how to process here, // or for any other reasons we need to send stuff for // // str is the raw string // // source is either 0 (SOURCE_SERVER) (server - from the real IRC server) or 1 (SOURCE_CLIENT) (client - from a real IRC client) // // Return 0 if something went wrong // Return 1 if everything OK int processrawstring(SSL *server_ssl, char *str, int source, struct client *clients, int sourcefd, struct ircdstate *ircdstate, struct channel *channels, struct settings *settings) { // Copy to a temporary string so we still have the original in case it's not processed char *strcopy = strdup(str); // Keep track of initial pointer for free()ing later char *strcopyPtr = strcopy; debugprint(DEBUG_FULL, "processrawstring(): Source type %d sent: \"%s\". Processing it...\n", source, strcopy); // Track which CLRF-separated message within this string we're on int messagecount = 0; // Build array of each space-separated token char messages[MAXTOKENS][MAXDATASIZE]; // Split the string by CRLF and add each CRLF-separated IRC message to an array char *token; while ((token = strsep(&strcopy, "\r\n")) != NULL) { if (*token == '\0') continue; // Skip consecutive matches if (messagecount >= MAXTOKENS) break; // Too many tokens debugprint(DEBUG_FULL, "String Token: \"%s\", length %zd.\n", token, strlen(token)); // Copy into the token array (strlen + 1 to get the NULL terminator) strncpy(messages[messagecount], token, strlen(token) + 1); messagecount++; } free(strcopyPtr); // If there is a previous possibly truncated message still in the holding area, the prepend that to the first message of this new lot // (Only if source was the server since we always strip \r\n from client messages when recving - TODO - Should we be doing that? if (ircdstate->currentmsg[0] && source == SOURCE_SERVER) { // Make a copy since we can't have source and destination the same with snprintf char *strtmp = strdup(messages[0]); debugprint(DEBUG_FULL, "processrawstring(): Previous truncated message detected, combining old '%s' with new '%s'...\n", ircdstate->currentmsg, strtmp); snprintf(messages[0], strlen(ircdstate->currentmsg) + strlen(strtmp) + 1, "%s%s", ircdstate->currentmsg, strtmp); messages[0][strlen(ircdstate->currentmsg) + strlen(strtmp)] = '\0'; // Make sure it's null terminated debugprint(DEBUG_FULL, "...into new string '%s' and clearing currentmsg holding area.\n", messages[0]); ircdstate->currentmsg[0] = '\0'; free(strtmp); } // If the final characters of the raw string weren't \r\n then assume the final token is a truncated message // Copy to a holding area for continuation next time // (Only if source was the server since we always strip \r\n from client messages when recving - TODO - Should we be doing that? if ((str[strlen(str)-2] != 13 || str[strlen(str)-1] != 10) && source == SOURCE_SERVER) { debugprint(DEBUG_FULL, "processrawstring(): Truncated message detected, storing final token '%s' for later.\n", messages[messagecount - 1]); strncpy(ircdstate->currentmsg, messages[messagecount - 1], strlen(messages[messagecount - 1])); ircdstate->currentmsg[strlen(messages[messagecount - 1])] = '\0'; // Remove it from the message count so it's not processed time time messagecount--; } else { // Otherwise, clear the holding area ircdstate->currentmsg[0] = '\0'; } // Go through each message, figure out what it is and if we're doing anything with it for (int i = 0; i < messagecount; i++) { // Copy to a temporary string so we still have the original in case it's not processed char *messagecopy = strdup(messages[i]); if (processircmessage(server_ssl, messagecopy, source, clients, sourcefd, ircdstate, channels, settings)) { debugprint(DEBUG_FULL, "Message processed: \"%s\", NULLing...\n", messages[i]); messages[i][0] = '\0'; } free(messagecopy); } // Deal with any unprocessed messages (send on to server/client(s) since we haven't dealt with them) for (int i = 0; i < messagecount; i++) { if (messages[i][0] != '\0') { debugprint(DEBUG_FULL, "Message %d (\"%s\") remains unprocessed...\n", i, messages[i]); switch(source) { case SOURCE_SERVER: // If message(s) were from the real IRC server // Relay/send to all clients ("except" = 0 because this should send to all clients) // TODO - Is this really going to send the original string if we have messed it with it in processrawstring() and friends!? debugprint(DEBUG_FULL, "bouncer-server: sending unprocessed server message '%s' to all clients, length %zd.\n", messages[i], strlen(messages[i])); sendtoallclients(clients, messages[i], EXCEPT_NONE, settings); break; case SOURCE_CLIENT: // If message(s) were from a real IRC client // Send to server debugprint(DEBUG_FULL, "bouncer-client: sending unprocessed client message '%s' to the server, length %zd.\n", messages[i], strlen(messages[i])); sendtoserver(server_ssl, messages[i], strlen(messages[i]), sourcefd, clients, settings); debugprint(DEBUG_FULL, "bouncer-client: sending unprocessed client message '%s' to all other clients, length %zd.\n", messages[i], strlen(messages[i])); // send the same thing to all *other* clients (all except for source fd) sendtoallclients(clients, messages[i], sourcefd, settings); break; default: fprintf(stderr, "Unexpected raw IRC string source for unprocessed message \"%s\", length %zd.!\n", messages[i], strlen(messages[i])); debugprint(DEBUG_CRIT, "Unexpected raw IRC string source for unprocessed message \"%s\", length %zd.!\n", messages[i], strlen(messages[i])); return 0; } } } debugprint(DEBUG_FULL, "Done with processrawstring()\n"); return 1; } // Where the big bouncing loop is 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 int fdmax; // highest numbered socket fd socklen_t addrlen; // client remote address size char remoteIP[INET6_ADDRSTRLEN]; // remote IP (assume up to IPv6 size) int newfd; // newly accept()ed socket descriptor struct sockaddr_storage remoteaddr; // client address int clientnumbytes; 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 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]; // Set all the clients to be "not connected", "not authenticated", "not registered", "not pending replies" for (int i = 0; i < MAXCLIENTS; i++) { clients[i].fd = 0; clients[i].authed = 0; clients[i].registered = 0; clients[i].pendingchannelmode = 0; clients[i].pendingban = 0; clients[i].pendingwho = 0; clients[i].pendinglist = 0; clients[i].pendingwhois = 0; clients[i].pendingwhowas = 0; clients[i].pendingnames = 0; clients[i].pendingcap = 0; } // 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 ircdstate ircdstate; // Struct of channels we're in struct channel *channels; channels = malloc(sizeof(struct channel) * MAXCHANNELS); // Set initial channel names to empty strings 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) ircdstate.oldnick[0] = '\0'; ircdstate.reconnecting = 0; connecttoircserver(&serverctx, &server_ssl, serversockfd, &ircdstate, 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; // If using client TLS if (settings->clienttls) { // Set up and configure client OpenSSL context ctx = create_openssl_context(SOURCE_CLIENT); 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 SIGHUP (for rehashing), SIGINT (Ctrl-C), and SIGTERM (default signal of `kill`) signal(SIGHUP, sighandler); // SIGHUP (1) signal(SIGINT, sighandler); // SIGINT (2) signal(SIGTERM, sighandler); // SIGTERM (15) // Block those signals sigset_t sigset, oldset; sigemptyset(&sigset); sigaddset(&sigset, SIGHUP); 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 // Add STDIN (fd 0) to read fds to monitor if we're not in background/daemon mode if (!settings->background) { FD_SET(STDIN, &rfds); } FD_SET(*serversockfd, &rfds); // add our server network socket to monitor FD_SET(*clientsockfd, &rfds); // add our client network socket to monitor // Add all connected clients to monitor (only add ones that are connected (clients[i] > 0)) // TODO - make sure *serversockfd stays at the same value (probably 3?) in all cases - what if the server disconnects/reconnects/etc. // TODO - now that only connected clients are monitored, perhaps tracking using both fdmax and num_client loops is unnecessary? for (int i = 0; i < MAXCLIENTS; i++) { if (clients[i].fd > 0) { debugprint(DEBUG_FULL, "monitoring fd %d.\n", clients[i].fd); FD_SET(clients[i].fd, &rfds); } } 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 addition to fd monitoring) struct timespec timeout = {SELECTTIMEOUT, 0}; // pselect() should timeout after SELECTTIMEOUT seconds int selret = pselect(fdmax + 1, &rfds, NULL, NULL, &timeout, &oldset); // network socket + 1, rfds, no writes, no exceptions/errors, no timeout, original sigmask if (selret < 0) { // Error or signal interrupt if (errno == EINTR) { // Signal caught, do signal handling debugprint(DEBUG_CRIT, "signal '%d' happened, exiting!\n", signum); if (signum == SIGHUP) { // REHASH requested // TODO - This code is duplicated between here and BLABOUNCER REHASH handling char outgoingmsg[MAXDATASIZE]; char failuremsg[MAXDATASIZE]; failuremsg[0] = '\0'; // Try to rehash... if (!rehash(settings, failuremsg)) { // ...or log and tell all clients if it failed debugprint(DEBUG_CRIT, "SIGHUP REHASH failed: %s.\n", failuremsg); if (!snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :SIGHUP REHASH failed: %s.", ircdstate.ircnick, failuremsg)) { debugprint(DEBUG_CRIT, "Error while preparing SIGHUP REHASH failure message response!\n"); outgoingmsg[0] = '\0'; } sendtoallclients(clients, outgoingmsg, 0, settings); } else { // ...or tell all clients it worked snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :SIGUP REHASH complete!", ircdstate.ircnick); sendtoallclients(clients, outgoingmsg, 0, settings); } // Then go back to the top of the loop continue; } else if (signum == SIGINT) { // Probably Ctrl+C cleanexit(server_ssl, clients, 0, &ircdstate, settings, "SIGINT received"); } else if (signum == SIGTERM) { // Probably `kill` cleanexit(server_ssl, clients, 0, &ircdstate, settings, "SIGTERM received"); } else { cleanexit(server_ssl, clients, 0, &ircdstate, settings, "Unexpected signal received"); } } else { // Some other error perror("pselect"); debugprint(DEBUG_CRIT, "pselect() error, errno '%d'.\n", errno); continue; } } // pselect() timed out, let's see if a server timeout might be happening, if not, just continue to the top of the loop again if (selret == 0) { debugprint(DEBUG_CRIT, "pselect() timed out.\n", errno); // SERVERTIMEOUT seconds have expired... if (ircdstate.lastmessagetime < time(NULL) - SERVERTIMEOUT && !FD_ISSET(*serversockfd, &rfds)) { if (ircdstate.timeoutcheck == 0) { // ...and we haven't tried a PING yet, so let's PING the server to see if things are still working debugprint(DEBUG_CRIT, "Server might have timed out after %ld seconds, PINGing it...\n", time(NULL) - ircdstate.lastmessagetime); ircdstate.timeoutcheck = 1; char outgoingmsg[MAXDATASIZE]; if (!snprintf(outgoingmsg, MAXDATASIZE, "PING %s", ircdstate.ircdname)) { fprintf(stderr, "Error while preparing timeout testing PING message!\n"); debugprint(DEBUG_CRIT, "Error while preparing timeout testing PING message\n"); snprintf(outgoingmsg, MAXDATASIZE, "PING timeouttest"); } sendtoserver(server_ssl, outgoingmsg, strlen(outgoingmsg), 0, clients, settings); // Back to top of loop continue; } else { // ...and we've already PINGed the server and haven't heard back yet, so let's assume we've timed out // TODO - Code duplication, make a function and share with socket error code below debugprint(DEBUG_CRIT, "Server has timed out (%ld seconds), reconnecting!\n", time(NULL) - ircdstate.lastmessagetime); // Tell all clients if we timed out char alertmsg[MAXDATASIZE]; snprintf(alertmsg, MAXDATASIZE, "NOTICE %s :Server has timed out (%ld seconds), reconnecting!", ircdstate.ircnick, time(NULL) - ircdstate.lastmessagetime); sendtoallclients(clients, alertmsg, 0, settings); if (settings->servertls) { // Finish up with OpenSSL if using server TLS if (server_ssl != NULL) SSL_free(server_ssl); // Set to NULL so we can check if we already free()d this on a previous attempt server_ssl = NULL; } // Close the socket close(*serversockfd); // Make a new one and reconnect if ((*serversockfd = createserversocket(settings->ircserver, settings->ircserverport)) == -1) { // We failed debugprint(DEBUG_CRIT, "dochat(): Couldn't reconnect to server, will try again.\n"); // Tell clients char alertmsg[MAXDATASIZE]; snprintf(alertmsg, MAXDATASIZE, "NOTICE %s :Couldn't reconnect to server, will try again.", ircdstate.ircnick); sendtoallclients(clients, alertmsg, 0, settings); // Don't go back to the top of the loop yet, perhaps some clients are waiting to tell us something - TODO - Could this ever happen in this state? } else { // We succeeded // Set reconnection marker for other functions to know we're reconnecting ircdstate.reconnecting = 1; // Set oldnick in case we change nick when reconnecting so we can inform existing clients strcpy(ircdstate.oldnick, ircdstate.ircnick); connecttoircserver(&serverctx, &server_ssl, serversockfd, &ircdstate, settings, clients); // Back to top of loop continue; } } } else { // No timeout is occuring, the pselect() just timed out (which is fine and expected), back to the top of the loop 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. // (although actually stdin may go once its not wanted for possible direct interaction for debugging) // See if there's anything to read from the server side (the real IRCd) if (FD_ISSET(*serversockfd, &rfds)) { debugprint(DEBUG_FULL, "reading server socket!\n"); if ((servernumbytes = sockread(server_ssl, serverbuf, MAXRCVSIZE - 1, settings->servertls)) == -1) { debugprint(DEBUG_CRIT, "dochat(): server sockread() error (-1) (%s), skipping loop iteration.\n", strerror(errno)); } else if (servernumbytes == 0) { debugprint(DEBUG_CRIT, "dochat(): server sockread() closed or no data received (0) (%s), skipping loop iteration.\n", strerror(errno)); } // 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) { debugprint(DEBUG_CRIT, "Server socket had an error (sockread return code %d), reconnecting!\n", servernumbytes); // Tell all clients if we had an error char alertmsg[MAXDATASIZE]; snprintf(alertmsg, MAXDATASIZE, "NOTICE %s :Server socket had an error (sockread return code %d), reconnecting!", ircdstate.ircnick, servernumbytes); sendtoallclients(clients, alertmsg, 0, settings); if (settings->servertls) { // Finish up with OpenSSL if using server TLS if (server_ssl != NULL) SSL_free(server_ssl); // Set to NULL so we can check if we already free()d this on a previous attempt server_ssl = NULL; } // Close the socket close(*serversockfd); // Make a new one and reconnect if ((*serversockfd = createserversocket(settings->ircserver, settings->ircserverport)) == -1) { // We failed debugprint(DEBUG_CRIT, "dochat(): Couldn't reconnect to server, will try again.\n"); // Tell clients char alertmsg[MAXDATASIZE]; snprintf(alertmsg, MAXDATASIZE, "NOTICE %s :Couldn't reconnect to server, will try again.", ircdstate.ircnick); sendtoallclients(clients, alertmsg, 0, settings); // Don't go back to the top of the loop yet, perhaps some clients are waiting to tell us something - TODO - Could this ever happen in this state? } else { // We succeeded // Set reconnection marker for other functions to know we're reconnecting ircdstate.reconnecting = 1; // Set oldnick in case we change nick when reconnecting so we can inform existing clients strcpy(ircdstate.oldnick, ircdstate.ircnick); connecttoircserver(&serverctx, &server_ssl, serversockfd, &ircdstate, settings, clients); // Back to top of loop continue; } } serverbuf[servernumbytes] = '\0'; debugprint(DEBUG_SOME, "BOUNCER-SERVER RECEIVED: '%s', length '%d'.\n", serverbuf, servernumbytes); // Try to process received string (which should contain one or more server responses/commands) // TODO - What if there were two server respones/commands and only one didn't need relaying? if (!processrawstring(server_ssl, serverbuf, SOURCE_SERVER, clients, EXCEPT_NONE, &ircdstate, channels, settings)) { fprintf(stderr, "Error: bouncer-server failed to process raw string.\n"); debugprint(DEBUG_CRIT, "Error: bouncer-server failed to process raw string.\n"); } } // see if there's anything from stdin (unless we're in background/daemon mode) if (!settings->background && FD_ISSET(STDIN, &rfds)) { debugprint(DEBUG_FULL, "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) { printf("\nError! No input.\n"); } else if (outgoingmsgrc == TOO_LONG) { printf ("Error! Too long. Would have allowed up to: [%s]\n", outgoingmsg); } // STDIN based commands for debugging if (strncmp(outgoingmsg, "listchannels", strlen("listchannels")) == 0) { printf("STDIN command starting: listchannels\n"); int channelcount = getchannelcount(channels); for (int i = 0; i < channelcount; i++) { printf("Checking channel[%d] out of %d.\n", i, channelcount); // Skip this one and increment channelcount if it's a blank channel if (!channels[i].name[0]) { debugprint(DEBUG_FULL, "Skipping channel[%d], incrementing channelcount.\n", i); channelcount++; continue; } printf("Found channel '%s'.\n", channels[i].name); } printf("STDIN command complete: listchannels\n"); continue; } // sourcefd = 0 as this is a trusted message sendtoserver(server_ssl, outgoingmsg, strlen(outgoingmsg), 0, clients, settings); } // go through all the remaining sockets to see if there's anything from the client sockets (either new connections or existing clients sending messages) // (clear newfd before doing this so we can tell if we're querying a new client or not) newfd = 0; for (int i = *clientsockfd; i <= fdmax; i++) { // skip if newfd is the current iteration of this loop, since we know we have just accept()ed it if (i == newfd) { continue; } debugprint(DEBUG_FULL, "checking client socket %d out of %d.\n", i, fdmax); if (FD_ISSET(i, &rfds)) { debugprint(DEBUG_FULL, "fd %d is FD_ISSET and it is a...\n", i); // if value of clientsockfd then must be a new connection, if greater must be an existing connection if (i == *clientsockfd) { debugprint(DEBUG_SOME, "...new connection!\n"); // handle new connections if (numclients(clients) >= MAXCLIENTS) { fprintf(stderr, "too many clients, disconnecting and skipping loop iteration!\n"); debugprint(DEBUG_CRIT, "too many clients, disconnecting and skipping loop iteration!\n"); disconnectclient(i, clients, &ircdstate, settings); continue; } addrlen = sizeof remoteaddr; newfd = accept(*clientsockfd, (struct sockaddr *)&remoteaddr, &addrlen); if (newfd == -1) { // something went wrong when accept()ing perror("accept"); } else { FD_SET(newfd, &rfds); // add to master set // TODO - needed? going to be re-done at the top anyway... if (newfd > fdmax) { // keep track of the max fdmax = newfd; } // Find a free element in the clients array and set to new fd value for (int j = 0; j < MAXCLIENTS; j++) { if (clients[j].fd == 0) { clients[j].fd = newfd; // Ensure its authentication status is set to 0 clients[j].authed = 0; // If using TLS then... if (settings->clienttls) { // ...set as OpenSSL FD and SSL_accept it clients[j].ssl = SSL_new(ctx); SSL_set_fd(clients[j].ssl, newfd); if (SSL_accept(clients[j].ssl) <= 0) { debugprint(DEBUG_CRIT, "SSL_accept failed for fd %d.\n", clients[j].fd); ERR_print_errors_fp(stderr); } else { debugprint(DEBUG_FULL, "SSL_accept succeeded for fd %d.\n", clients[j].fd); } } else { // If not using TLS then cast newfd to SSL* even though it will just be the original newfd int really clients[j].ssl = (SSL*)(long int)newfd; } break; } } // TODO - Handle the "find a free element" loop not finding a free element debugprint(DEBUG_FULL, "bouncer-client: new connection from %s on socket %d\n", inet_ntop(remoteaddr.ss_family, get_in_addr((struct sockaddr*)&remoteaddr), remoteIP, INET6_ADDRSTRLEN), newfd); // Alert other clients about the new connection char alertmsg[MAXDATASIZE]; if (!snprintf(alertmsg, MAXDATASIZE, "NOTICE %s :blabouncer: new client connected from %s with fd %d.", ircdstate.ircnick, inet_ntop(remoteaddr.ss_family, get_in_addr((struct sockaddr*)&remoteaddr), remoteIP, INET6_ADDRSTRLEN), newfd)) { fprintf(stderr, "Error while preparing new client connection NOTICE!\n"); debugprint(DEBUG_CRIT, "Error while preparing new client connection NOTICE!\n"); alertmsg[0] = '\0'; } // "except" 0 since we trust this message sendtoallclients(clients, alertmsg, 0, settings); debugprint(DEBUG_FULL, "bouncer-client: total client connections: %d\n", numclients(clients)); } } else { debugprint(DEBUG_FULL, "...previous connection!\n"); // handle data from a client if ((clientnumbytes = sockread(clients[arrindex(clients, i)].ssl, clientbuf, sizeof clientbuf, settings->clienttls)) <= 0) { // got error or connection closed by client if (clientnumbytes == 0) { // connection closed debugprint(DEBUG_SOME, "bouncer-client: socket %d hung up\n", i); } else { debugprint(DEBUG_CRIT, "dochat(): client sockread() error fd '%d'.\n", i); } // Disconnect the client disconnectclient(i, clients, &ircdstate, settings); FD_CLR(i, &rfds); // remove from master set - TODO is this needed at the moment since we just add everything from *clientsockfd to fdmax to rfds // TODO - Handle the "remove the client" loop not finding the old fd debugprint(DEBUG_FULL, "bouncer-client: total client connections: %d\n", numclients(clients)); } else { // we got some data from a client // null terminate that baby clientbuf[clientnumbytes] = '\0'; // TODO make sure this can't overrun if some super long line (max bytes?) was received // clear up any newlines - TODO - Should we be doing this? If not, we can stop only doing truncation checks for the server in processrawstring(). while (clientbuf[strlen(clientbuf) - 1] == '\n' || clientbuf[strlen(clientbuf) - 1] == '\r') { clientbuf[strlen(clientbuf) - 1] = '\0'; } debugprint(DEBUG_SOME, "BOUNCER-CLIENT RECEIVED: '%s'\n", clientbuf); // Try to process received string (which should contain one or more client responses/commands) // TODO - What if there were two server respones/commands and only one didn't need relaying? if (!processrawstring(server_ssl, clientbuf, SOURCE_CLIENT, clients, i, &ircdstate, channels, settings)) { fprintf(stderr, "Error: bouncer-client failed to process raw string.\n"); debugprint(DEBUG_CRIT, "Error: bouncer-client failed to process raw string.\n"); } } } } } } free(channels); } int main(int argc, char *argv[]) { // Structure of our various settings which are to either be read from the configuration file or set at runtime struct settings settings; // Terminate our global debug file string in case it's referenced before being read from file debugpath[0] = '\0'; // Assume background/daemon mode unless specified otherwise on the command line settings.background = 1; // Set configuration file to string to blank for now settings.conffile[0] = '\0'; // Check to see what was specified on the command line // TODO - Do better command line argument handling (no required order) char helptext[] = "usage: %s [-f] [-c /path/to/blabouncer.conf] (-f for foreground mode)\n"; if (argc == 2) { if (strcmp(argv[1], "-f")) { fprintf(stderr, helptext, argv[0]); exit(1); } else { settings.background = 0; } } else if (argc == 3) { if (strcmp(argv[1], "-c")) { fprintf(stderr, helptext, argv[0]); exit(1); } else { strcpy(settings.conffile, argv[2]); } } else if (argc == 4) { if (strcmp(argv[1], "-f") || strcmp(argv[2], "-c")) { fprintf(stderr, helptext, argv[0]); exit(1); } else { settings.background = 0; strcpy(settings.conffile, argv[3]); } } else if (argc > 4) { fprintf(stderr, helptext, argv[0]); exit(1); } // If a configuration file wasn't set on the command line, set it now if (!settings.conffile[0]) { snprintf(settings.conffile, PATH_MAX, "%s/.blabouncer/blabouncer.conf", getenv("HOME")); // Since this is the default, it might not exist yet, so let's check... struct stat st = {0}; if (stat(settings.conffile, &st) == -1) { // ...it doesn't exist, so let's create it printf("Creating default configuration file '%s'.\n", settings.conffile); createconfigfile(settings.conffile); } } // Populate settings from configuration file // TODO - Try to share some/all of this code with the rehash() settings loading // What is the auto replay mode? if (!getconfstr("replaymode", settings.conffile, settings.replaymode)) { printf("main(): error getting 'replaymode' from configuration file.\n"); exit(1); } else { if (strcmp(settings.replaymode, "none") && strcmp(settings.replaymode, "time") && strcmp(settings.replaymode, "lastspoke")) { printf("main(): replaymode in configuration file must be one of \"none\", \"time\", or \"lastspoke\".\n"); exit(1); } } // How many seconds of replay log should automatically be replayed - TODO - Can we do error checking on this? settings.replayseconds = getconfint("replayseconds", settings.conffile); if (errno == ECONFINT) { printf("main(): error getting 'replayseconds' from configuration file.\n"); exit(1); } // What port should the bouncer listen on if (!getconfstr("clientport", settings.conffile, settings.clientport)) { printf("main(): error getting 'clientport' from configuration file.\n"); exit(1); } // What is the configured nick? if (!getconfstr("nick", settings.conffile, settings.ircnick)) { printf("main(): error getting 'nick' from configuration file.\n"); exit(1); } // What is the configured username? if (!getconfstr("username", settings.conffile, settings.ircusername)) { printf("main(): error getting 'username' from configuration file.\n"); exit(1); } // What is the configured real name? if (!getconfstr("realname", settings.conffile, settings.ircrealname)) { printf("main(): error getting 'realname' from configuration file.\n"); exit(1); } // What, if anything, are the configured auto channels? if (!getconfstr("channels", settings.conffile, settings.autochannels)) { settings.autochannels[0] = '\0'; } else { // If something was set, make sure it's not too long if (strlen(settings.autochannels) >= MAXAUTOCHANLEN) { printf("main(): 'channels' option in configuration file is too long.\n"); exit(1); } } // What is the real IRC server address? if (!getconfstr("ircserver", settings.conffile, settings.ircserver)) { printf("main(): error getting 'ircserver' from configuration file.\n"); exit(1); } // What is the real IRC server port? if (!getconfstr("ircserverport", settings.conffile, settings.ircserverport)) { printf("main(): error getting 'ircserverport' from configuration file.\n"); exit(1); } // What is the real IRC server password, if any? if (!getconfstr("ircserverpassword", settings.conffile, settings.ircserverpassword)) { settings.ircserverpassword[0] = '\0'; } // What is the connect command, if any? if (!getconfstr("connectcommand", settings.conffile, settings.connectcommand)) { settings.connectcommand[0] = '\0'; } // Is the base directory set? If not, use the default. if (!getconfstr("basedir", settings.conffile, settings.basedir)) { snprintf(settings.basedir, PATH_MAX, "%s/.blabouncer/", getenv("HOME")); } // Should the bouncer use TLS for the IRC server? settings.servertls = getconfint("servertls", settings.conffile); if (errno == ECONFINT) { printf("main(): error getting 'servertls' from configuration file.\n"); exit(1); } // Should the bouncer use TLS for clients? settings.clienttls = getconfint("clienttls", settings.conffile); if (errno == ECONFINT) { printf("main(): error getting 'clienttls' from configuration file.\n"); exit(1); } // If so, load the certificates if (settings.clienttls) { // What is the certificate file path? if (!getconfstr("certfile", settings.conffile, settings.certfile)) { // If none provided, set to default if (!snprintf(settings.certfile, PATH_MAX, "%s/cert.pem", settings.basedir)) { fprintf(stderr, "Error while preparing default certfile location!\n"); exit(1); } } // What is the certificate key file path? if (!getconfstr("keyfile", settings.conffile, settings.keyfile)) { // If none provided, set to default if (!snprintf(settings.keyfile, PATH_MAX, "%s/key.pem", settings.basedir)) { fprintf(stderr, "Error while preparing default certfile location!\n"); exit(1); } } } // Make sure the base directory exists struct stat st = {0}; if (stat(settings.basedir, &st) == -1) { if (mkdir(settings.basedir, 0700)) { printf("Error creating base directory '%s'.\n", settings.basedir); exit(1); } else { printf("Created base directory '%s'.\n", settings.basedir); } } // Is logging enabled? settings.logging = getconfint("logging", settings.conffile); if (errno == ECONFINT) { printf("main(): error getting 'logging' from configuration file.\n"); exit(1); } // Is replay logging enabled? settings.replaylogging = getconfint("replaylogging", settings.conffile); if (errno == ECONFINT) { printf("main(): error getting 'replaylogging' from configuration file.\n"); exit(1); } // How many debug logs should we keep? settings.debugkeep = getconfint("debugkeep", settings.conffile); if (errno == ECONFINT) { printf("main(): error getting 'debugkeep' from configuration file.\n"); exit(1); } // Is debugging enabled? debug = getconfint("debug", settings.conffile); if (errno == ECONFINT) { printf("main(): error getting 'debug' from configuration file.\n"); exit(1); } if (!snprintf(debugpath, PATH_MAX, "%s/debug/debug.txt", settings.basedir)) { fprintf(stderr, "Error while preparing debug path location!\n"); exit(1); } // debugfile goes in its own directory char debugdir[PATH_MAX]; if (!snprintf(debugdir, PATH_MAX, "%s/debug/", settings.basedir)) { fprintf(stderr, "Error while preparing debug path location!\n"); exit(1); } // Make sure the debug directory exists if (stat(debugdir, &st) == -1) { if (mkdir(debugdir, 0700)) { printf("Error creating debug directory '%s'.\n", debugdir); exit(1); } else { printf("Created debug directory '%s'.\n", debugdir); } } // Prepare the debug file // (Keep settings.debugkeep number of debug files around for past debugging) if (settings.debugkeep > 0) { // Check each possible debug file and rename it to one higher char tmppath[PATH_MAX]; char tmppathnew[PATH_MAX]; // Delete or rename numbered debug files first for (int i = settings.debugkeep; i > 0; i--) { if (!snprintf(tmppath, PATH_MAX - 1, "%s.%d", debugpath, i)) { fprintf(stderr, "Error while preparing to check old debug files!\n"); exit(1); } if (!access(tmppath, F_OK)) { if (i == settings.debugkeep) { if (remove(tmppath)) { printf("error deleting old debug file '%s'.\n", tmppath); exit(1); } } else { if (!snprintf(tmppathnew, PATH_MAX - 1, "%s.%d", debugpath, i + 1)) { fprintf(stderr, "Error while preparing to rename old debug file!\n"); exit(1); } if (rename(tmppath, tmppathnew)) { printf("error renaming old debug file '%s' to '%s'.\n", tmppath, tmppathnew); exit(1); } } } } // Rename the previous debug file next if (!access(debugpath, F_OK)) { if (rename(debugpath, tmppath)) { printf("error renaming old debug file '%s' to '%s'.\n", debugpath, tmppath); exit(1); } } } // Write a friendly debug message to file (even if debug to file is disabled) // Prepare a friendly time string 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'; // Note the old debug setting int debugold = debug; // Temporarily enable debugging to file debug = 1; // Print it debugprint(DEBUG_CRIT, "blabouncer started at %s, debugging is set to %d.\n", timestr, debugold); // Set debugging back to whatever it was debug = debugold; debugprint(DEBUG_SOME, "Using configuration file '%s'.\n", settings.conffile); // Unless specified otherwise on the command line, fork to background if (settings.background) { pid_t pid, sid; // Fork from parent if ((pid = fork()) < 0) { printf("Couldn't fork, exiting.\n"); debugprint(DEBUG_CRIT, "Couldn't fork, exiting.\n"); exit(1); } // Exit parent (pid will be > 0 for the child) if (pid > 0) { exit(0); } umask(0); // Let child become process group leader if ((sid = setsid()) < 0) { printf("Couldn't setsid() child, exiting.\n"); debugprint(DEBUG_CRIT, "Couldn't setsid() child, exiting.\n"); exit(1); } // In case our cwd changes chdir("/"); // TODO - close() STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO here, might need to change fd number // logic/assumptions elsewhere (such as fd 3 being server, fd 4 being clients, etc.) } // TODO: see if any of this can be shared (i.e. 1. avoid code duplication, and 2. see if variables can be shared between client/server sockets) // I will try to keep to the notation of "server" meaning the real IRCd, "bouncer" meaning the bouncer, and "client" meaning the real IRC client // Create server socket int serversockfd; if ((serversockfd = createserversocket(settings.ircserver, settings.ircserverport)) == -1) { debugprint(DEBUG_CRIT, "main(): Couldn't connect to server, exiting.\n"); exit(1); } // Create client socket (after server so we can use its fd number later as fdmax) int clientsockfd = createclientsocket(settings.clientport); dochat(&serversockfd, &clientsockfd, &settings); printf("dochat() complete, closing socket...\n"); close(serversockfd); return 0; }