/* * zyklonb.c: the experimental IRC bot * * Copyright (c) 2014, Přemysl Janouch <p.janouch@gmail.com> * All rights reserved. * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * */ #define PROGRAM_NAME "ZyklonB" #define PROGRAM_VERSION "alpha" #include "common.c" // --- Configuration (application-specific) ------------------------------------ static struct config_item g_config_table[] = { { "nickname", "ZyklonB", "IRC nickname" }, { "username", "bot", "IRC user name" }, { "realname", "ZyklonB IRC bot", "IRC real name/e-mail" }, { "irc_host", NULL, "Address of the IRC server" }, { "irc_port", "6667", "Port of the IRC server" }, { "ssl_use", "off", "Whether to use SSL" }, { "ssl_cert", NULL, "Client SSL certificate (PEM)" }, { "autojoin", NULL, "Channels to join on start" }, { "reconnect", "on", "Whether to reconnect on error" }, { "reconnect_delay", "5", "Time between reconnecting" }, { "prefix", ":", "The prefix for bot commands" }, { "admin", NULL, "Host mask for administrators" }, { "plugins", NULL, "The plugins to load on startup" }, { "plugin_dir", NULL, "Where to search for plugins" }, { "recover", "on", "Whether to re-launch on crash" }, { NULL, NULL, NULL } }; // --- Application data -------------------------------------------------------- struct plugin_data { LIST_HEADER (plugin_data) struct bot_context *ctx; ///< Parent context char *name; ///< Plugin identifier pid_t pid; ///< PID of the plugin process bool is_zombie; ///< Whether the child is a zombie bool initialized; ///< Ready to exchange IRC messages struct str queued_output; ///< Output queued up until initialized // Since we're doing non-blocking I/O, we need to queue up data so that // we don't stall on plugins unnecessarily. int read_fd; ///< The read end of the comm. pipe int write_fd; ///< The write end of the comm. pipe struct str read_buffer; ///< Unprocessed input struct str write_buffer; ///< Output yet to be sent out }; static void plugin_data_init (struct plugin_data *self) { memset (self, 0, sizeof *self); self->pid = -1; str_init (&self->queued_output); self->read_fd = -1; str_init (&self->read_buffer); self->write_fd = -1; str_init (&self->write_buffer); } static void plugin_data_free (struct plugin_data *self) { soft_assert (self->pid == -1); free (self->name); str_free (&self->read_buffer); if (!soft_assert (self->read_fd == -1)) xclose (self->read_fd); str_free (&self->write_buffer); if (!soft_assert (self->write_fd == -1)) xclose (self->write_fd); if (!self->initialized) str_free (&self->queued_output); } struct bot_context { struct str_map config; ///< User configuration regex_t *admin_re; ///< Regex to match our administrator int irc_fd; ///< Socket FD of the server struct str read_buffer; ///< Input yet to be processed bool irc_ready; ///< Whether we may send messages now SSL_CTX *ssl_ctx; ///< SSL context SSL *ssl; ///< SSL connection struct plugin_data *plugins; ///< Linked list of plugins struct str_map plugins_by_name; ///< Indexes @em plugins by their name struct poller poller; ///< Manages polled descriptors bool quitting; ///< User requested quitting bool polling; ///< The event loop is running }; static void bot_context_init (struct bot_context *self) { str_map_init (&self->config); self->config.free = free; load_config_defaults (&self->config, g_config_table); self->admin_re = NULL; self->irc_fd = -1; str_init (&self->read_buffer); self->irc_ready = false; self->ssl = NULL; self->ssl_ctx = NULL; self->plugins = NULL; str_map_init (&self->plugins_by_name); poller_init (&self->poller); self->quitting = false; self->polling = false; } static void bot_context_free (struct bot_context *self) { str_map_free (&self->config); if (self->admin_re) regex_free (self->admin_re); str_free (&self->read_buffer); // TODO: terminate the plugins properly before this is called struct plugin_data *link, *tmp; for (link = self->plugins; link; link = tmp) { tmp = link->next; plugin_data_free (link); free (link); } if (self->irc_fd != -1) xclose (self->irc_fd); if (self->ssl) SSL_free (self->ssl); if (self->ssl_ctx) SSL_CTX_free (self->ssl_ctx); str_map_free (&self->plugins_by_name); poller_free (&self->poller); } static void irc_shutdown (struct bot_context *ctx) { // Generally non-critical if (ctx->ssl) soft_assert (SSL_shutdown (ctx->ssl) != -1); else soft_assert (shutdown (ctx->irc_fd, SHUT_WR) == 0); } static void initiate_quit (struct bot_context *ctx) { irc_shutdown (ctx); ctx->quitting = true; } static void try_finish_quit (struct bot_context *ctx) { if (!ctx->quitting) return; if (ctx->irc_fd == -1 && !ctx->plugins) ctx->polling = false; } static bool irc_send (struct bot_context *ctx, const char *format, ...) ATTRIBUTE_PRINTF (2, 3); // XXX: is it okay to just ignore the return value and wait until we receive // it in on_irc_readable()? static bool irc_send (struct bot_context *ctx, const char *format, ...) { va_list ap; if (g_debug_mode) { fputs ("[IRC] <== \"", stderr); va_start (ap, format); vfprintf (stderr, format, ap); va_end (ap); fputs ("\"\n", stderr); } if (!soft_assert (ctx->irc_fd != -1)) return false; va_start (ap, format); struct str str; str_init (&str); str_append_vprintf (&str, format, ap); str_append (&str, "\r\n"); va_end (ap); bool result = true; if (ctx->ssl) { // TODO: call SSL_get_error() to detect if a clean shutdown has occured if (SSL_write (ctx->ssl, str.str, str.len) != (int) str.len) { print_debug ("%s: %s: %s", __func__, "SSL_write", ERR_error_string (ERR_get_error (), NULL)); result = false; } } else if (write (ctx->irc_fd, str.str, str.len) != (ssize_t) str.len) { print_debug ("%s: %s: %s", __func__, "write", strerror (errno)); result = false; } str_free (&str); return result; } static bool irc_initialize_ssl (struct bot_context *ctx, struct error **e) { ctx->ssl_ctx = SSL_CTX_new (SSLv23_client_method ()); if (!ctx->ssl_ctx) goto error_ssl_1; // We don't care; some encryption is always better than no encryption SSL_CTX_set_verify (ctx->ssl_ctx, SSL_VERIFY_NONE, NULL); // XXX: maybe we should call SSL_CTX_set_options() for some workarounds ctx->ssl = SSL_new (ctx->ssl_ctx); if (!ctx->ssl) goto error_ssl_2; const char *ssl_cert = str_map_find (&ctx->config, "ssl_cert"); if (ssl_cert) { char *path = resolve_config_filename (ssl_cert); if (!path) print_error ("%s: %s", "cannot open file", ssl_cert); // XXX: perhaps we should read the file ourselves for better messages else if (!SSL_use_certificate_file (ctx->ssl, path, SSL_FILETYPE_PEM)) print_error ("%s: %s", "setting the SSL client certificate failed", ERR_error_string (ERR_get_error (), NULL)); free (path); } SSL_set_connect_state (ctx->ssl); if (!SSL_set_fd (ctx->ssl, ctx->irc_fd)) goto error_ssl_3; // Avoid SSL_write() returning SSL_ERROR_WANT_READ SSL_set_mode (ctx->ssl, SSL_MODE_AUTO_RETRY); if (SSL_connect (ctx->ssl) > 0) return true; error_ssl_3: SSL_free (ctx->ssl); ctx->ssl = NULL; error_ssl_2: SSL_CTX_free (ctx->ssl_ctx); ctx->ssl_ctx = NULL; error_ssl_1: // XXX: these error strings are really nasty; also there could be // multiple errors on the OpenSSL stack. error_set (e, "%s: %s", "could not initialize SSL", ERR_error_string (ERR_get_error (), NULL)); return false; } static bool irc_establish_connection (struct bot_context *ctx, const char *host, const char *port, bool use_ssl, struct error **e) { struct addrinfo gai_hints, *gai_result, *gai_iter; memset (&gai_hints, 0, sizeof gai_hints); // We definitely want TCP. gai_hints.ai_socktype = SOCK_STREAM; int err = getaddrinfo (host, port, &gai_hints, &gai_result); if (err) { error_set (e, "%s: %s: %s", "connection failed", "getaddrinfo", gai_strerror (err)); return false; } int sockfd; for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next) { sockfd = socket (gai_iter->ai_family, gai_iter->ai_socktype, gai_iter->ai_protocol); if (sockfd == -1) continue; set_cloexec (sockfd); int yes = 1; soft_assert (setsockopt (sockfd, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof yes) != -1); const char *real_host = host; // Let's try to resolve the address back into a real hostname; // we don't really need this, so we can let it quietly fail char buf[NI_MAXHOST]; err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen, buf, sizeof buf, NULL, 0, 0); if (err) print_debug ("%s: %s", "getnameinfo", gai_strerror (err)); else real_host = buf; // XXX: we shouldn't mix these statuses with `struct error'; choose 1! print_status ("connecting to `%s:%s'...", real_host, port); if (!connect (sockfd, gai_iter->ai_addr, gai_iter->ai_addrlen)) break; xclose (sockfd); } freeaddrinfo (gai_result); if (!gai_iter) { error_set (e, "connection failed"); return false; } ctx->irc_fd = sockfd; if (use_ssl && !irc_initialize_ssl (ctx, e)) { xclose (ctx->irc_fd); ctx->irc_fd = -1; return false; } print_status ("connection established"); return true; } // --- Signals ----------------------------------------------------------------- static int g_signal_pipe[2]; ///< A pipe used to signal... signals static struct str_vector g_original_argv, ///< Original program arguments g_recovery_env; ///< Environment for re-exec recovery /// Program termination has been requested by a signal static volatile sig_atomic_t g_termination_requested; /// Points to startup reason location within `g_recovery_environment' static char **g_startup_reason_location; /// The environment variable used to pass the startup reason when re-executing static const char g_startup_reason_str[] = "STARTUP_REASON"; static void sigchld_handler (int signum) { (void) signum; int original_errno = errno; // Just so that the read end of the pipe wakes up the poller. // NOTE: Linux has signalfd() and eventfd(), and the BSD's have kqueue. // All of them are better than this approach, although platform-specific. if (write (g_signal_pipe[1], "c", 1) == -1) soft_assert (errno == EAGAIN); errno = original_errno; } static void sigterm_handler (int signum) { (void) signum; g_termination_requested = true; int original_errno = errno; if (write (g_signal_pipe[1], "t", 1) == -1) soft_assert (errno == EAGAIN); errno = original_errno; } static void setup_signal_handlers (void) { if (pipe (g_signal_pipe) == -1) exit_fatal ("%s: %s", "pipe", strerror (errno)); set_cloexec (g_signal_pipe[0]); set_cloexec (g_signal_pipe[1]); // So that the pipe cannot overflow; it would make write() block within // the signal handler, which is something we really don't want to happen. // The same holds true for read(). set_blocking (g_signal_pipe[0], false); set_blocking (g_signal_pipe[1], false); struct sigaction sa; sa.sa_flags = SA_RESTART; sa.sa_handler = sigchld_handler; sigemptyset (&sa.sa_mask); if (sigaction (SIGCHLD, &sa, NULL) == -1) exit_fatal ("sigaction: %s", strerror (errno)); signal (SIGPIPE, SIG_IGN); sa.sa_handler = sigterm_handler; if (sigaction (SIGINT, &sa, NULL) == -1 || sigaction (SIGTERM, &sa, NULL) == -1) exit_fatal ("sigaction: %s", strerror (errno)); } static void translate_signal_info (int no, const char **name, int code, const char **reason) { if (code == SI_USER) *reason = "signal sent by kill()"; if (code == SI_QUEUE) *reason = "signal sent by sigqueue()"; switch (no) { case SIGILL: *name = "SIGILL"; if (code == ILL_ILLOPC) *reason = "illegal opcode"; if (code == ILL_ILLOPN) *reason = "illegal operand"; if (code == ILL_ILLADR) *reason = "illegal addressing mode"; if (code == ILL_ILLTRP) *reason = "illegal trap"; if (code == ILL_PRVOPC) *reason = "privileged opcode"; if (code == ILL_PRVREG) *reason = "privileged register"; if (code == ILL_COPROC) *reason = "coprocessor error"; if (code == ILL_BADSTK) *reason = "internal stack error"; break; case SIGFPE: *name = "SIGFPE"; if (code == FPE_INTDIV) *reason = "integer divide by zero"; if (code == FPE_INTOVF) *reason = "integer overflow"; if (code == FPE_FLTDIV) *reason = "floating-point divide by zero"; if (code == FPE_FLTOVF) *reason = "floating-point overflow"; if (code == FPE_FLTUND) *reason = "floating-point underflow"; if (code == FPE_FLTRES) *reason = "floating-point inexact result"; if (code == FPE_FLTINV) *reason = "invalid floating-point operation"; if (code == FPE_FLTSUB) *reason = "subscript out of range"; break; case SIGSEGV: *name = "SIGSEGV"; if (code == SEGV_MAPERR) *reason = "address not mapped to object"; if (code == SEGV_ACCERR) *reason = "invalid permissions for mapped object"; break; case SIGBUS: *name = "SIGBUS"; if (code == BUS_ADRALN) *reason = "invalid address alignment"; if (code == BUS_ADRERR) *reason = "nonexistent physical address"; if (code == BUS_OBJERR) *reason = "object-specific hardware error"; break; default: *name = NULL; } } static void recovery_handler (int signum, siginfo_t *info, void *context) { (void) context; // TODO: maybe try to force a core dump like this: if (fork() == 0) return; // TODO: maybe we could even send "\r\nQUIT :reason\r\n" to the server. >_> // As long as we're not connected via TLS, that is. const char *signal_name = NULL, *reason = NULL; translate_signal_info (signum, &signal_name, info->si_code, &reason); char buf[128], numbuf[8]; if (!signal_name) { snprintf (numbuf, sizeof numbuf, "%d", signum); signal_name = numbuf; } if (reason) snprintf (buf, sizeof buf, "%s=%s: %s: %s", g_startup_reason_str, "signal received", signal_name, reason); else snprintf (buf, sizeof buf, "%s=%s: %s", g_startup_reason_str, "signal received", signal_name); *g_startup_reason_location = buf; // TODO: maybe pregenerate the path, see the following for some other ways // that would be illegal to do from within a signal handler: // http://stackoverflow.com/a/1024937 // http://stackoverflow.com/q/799679 // Especially if we change the current working directory in the program. // // Note that I can just overwrite g_orig_argv[0]. // NOTE: our children will read EOF on the read ends of their pipes as a // a result of O_CLOEXEC. That should be enough to make them terminate. char **argv = g_original_argv.vector, **argp = g_recovery_env.vector; execve ("/proc/self/exe", argv, argp); // Linux execve ("/proc/curproc/file", argv, argp); // BSD execve ("/proc/curproc/exe", argv, argp); // BSD execve ("/proc/self/path/a.out", argv, argp); // Solaris execve (argv[0], argv, argp); // unreliable fallback // Let's just crash perror ("execve"); signal (signum, SIG_DFL); raise (signum); } static void prepare_recovery_environment (void) { str_vector_init (&g_recovery_env); str_vector_add_vector (&g_recovery_env, environ); // Prepare a location within the environment where we will put the startup // (or maybe rather restart) reason in case of an irrecoverable error. char **iter; for (iter = g_recovery_env.vector; *iter; iter++) { const size_t len = sizeof g_startup_reason_str - 1; if (!strncmp (*iter, g_startup_reason_str, len) && (*iter)[len] == '=') break; } if (iter) g_startup_reason_location = iter; else { str_vector_add (&g_recovery_env, ""); g_startup_reason_location = g_recovery_env.vector + g_recovery_env.len - 1; } } static void setup_recovery_handler (struct bot_context *ctx) { const char *recover_str = str_map_find (&ctx->config, "recover"); hard_assert (recover_str != NULL); // We have a default value for this bool recover; if (!set_boolean_if_valid (&recover, recover_str)) { print_error ("invalid configuration value for `%s'", "recover"); exit (EXIT_FAILURE); } if (!recover) return; // Make sure these signals aren't blocked, otherwise we would be unable // to handle them, making the critical conditions fatal. sigset_t mask; sigemptyset (&mask); sigaddset (&mask, SIGSEGV); sigaddset (&mask, SIGBUS); sigaddset (&mask, SIGFPE); sigaddset (&mask, SIGILL); sigprocmask (SIG_UNBLOCK, &mask, NULL); struct sigaction sa; sa.sa_flags = SA_SIGINFO; sa.sa_sigaction = recovery_handler; sigemptyset (&sa.sa_mask); prepare_recovery_environment (); // TODO: also handle SIGABRT... or avoid doing abort() in the first place? if (sigaction (SIGSEGV, &sa, NULL) == -1 || sigaction (SIGBUS, &sa, NULL) == -1 || sigaction (SIGFPE, &sa, NULL) == -1 || sigaction (SIGILL, &sa, NULL) == -1) print_error ("sigaction: %s", strerror (errno)); } // --- Plugins ----------------------------------------------------------------- /// The name of the special IRC command for interprocess communication static const char *plugin_ipc_command = "ZYKLONB"; static struct plugin_data * plugin_find_by_pid (struct bot_context *ctx, pid_t pid) { struct plugin_data *iter; for (iter = ctx->plugins; iter; iter = iter->next) if (iter->pid == pid) return iter; return NULL; } static bool plugin_zombify (struct plugin_data *plugin) { if (plugin->is_zombie) return false; // FIXME: make sure that we don't remove entries from the poller while we // still may have stuff to read; maybe just check that the read pipe is // empty before closing it... and then on EOF check if `pid == -1' and // only then dispose of it (it'd be best to simulate that both of these // cases may happen). ssize_t poller_idx = poller_find_by_fd (&plugin->ctx->poller, plugin->write_fd); if (poller_idx != -1) poller_remove_at_index (&plugin->ctx->poller, poller_idx); // TODO: try to flush the write buffer (non-blocking)? // The plugin should terminate itself after it receives EOF. xclose (plugin->write_fd); plugin->write_fd = -1; // Make it a pseudo-anonymous zombie. In this state we process any // remaining commands it attempts to send to us before it finally dies. str_map_set (&plugin->ctx->plugins_by_name, plugin->name, NULL); plugin->is_zombie = true; return true; } static void on_plugin_writable (const struct pollfd *fd, struct plugin_data *plugin) { struct bot_context *ctx = plugin->ctx; struct str *buf = &plugin->write_buffer; size_t written_total = 0; if (fd->revents & ~(POLLOUT | POLLHUP | POLLERR)) print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents); while (written_total != buf->len) { ssize_t n_written = write (fd->fd, buf->str + written_total, buf->len - written_total); if (n_written < 0) { if (errno == EAGAIN) break; if (errno == EINTR) continue; soft_assert (errno == EPIPE); // Zombies shouldn't get dispatched for writability hard_assert (!plugin->is_zombie); print_debug ("%s: %s", "write", strerror (errno)); print_error ("failure on writing to plugin `%s'," " therefore I'm unloading it", plugin->name); plugin_zombify (plugin); break; } // This may be equivalent to EAGAIN on some implementations if (n_written == 0) break; written_total += n_written; } if (written_total != 0) str_remove_slice (buf, 0, written_total); if (buf->len == 0) { // Everything has been written, there's no need to end up in here again ssize_t index = poller_find_by_fd (&ctx->poller, fd->fd); if (index != -1) poller_remove_at_index (&ctx->poller, index); } } static void plugin_queue_write (struct plugin_data *plugin) { if (plugin->is_zombie) return; // Don't let the write buffer grow infinitely. If there's a ton of data // waiting to be processed by the plugin, it usually means there's something // wrong with it (such as someone stopping the process). if (plugin->write_buffer.len >= (1 << 20)) { print_warning ("plugin `%s' does not seem to process messages fast" " enough, I'm unloading it", plugin->name); plugin_zombify (plugin); return; } poller_set (&plugin->ctx->poller, plugin->write_fd, POLLOUT, (poller_dispatcher_func) on_plugin_writable, plugin); } static void plugin_send (struct plugin_data *plugin, const char *format, ...) ATTRIBUTE_PRINTF (2, 3); static void plugin_send (struct plugin_data *plugin, const char *format, ...) { va_list ap; if (g_debug_mode) { fprintf (stderr, "[%s] <-- \"", plugin->name); va_start (ap, format); vfprintf (stderr, format, ap); va_end (ap); fputs ("\"\n", stderr); } va_start (ap, format); str_append_vprintf (&plugin->write_buffer, format, ap); va_end (ap); str_append (&plugin->write_buffer, "\r\n"); plugin_queue_write (plugin); } static void plugin_process_message (const struct irc_message *msg, const char *raw, void *user_data) { struct plugin_data *plugin = user_data; struct bot_context *ctx = plugin->ctx; if (g_debug_mode) fprintf (stderr, "[%s] --> \"%s\"\n", plugin->name, raw); if (!strcasecmp (msg->command, plugin_ipc_command)) { // Replies are sent in the order in which they came in, so there's // no need to attach a special identifier to them. It might be // desirable in some cases, though. if (msg->params.len < 1) return; const char *command = msg->params.vector[0]; if (!plugin->initialized && !strcasecmp (command, "register")) { // Register for relaying of IRC traffic plugin->initialized = true; // Flush any queued up traffic here. The point of queuing it in // the first place is so that we don't have to wait for plugin // initialization during startup. // // Note that if we start filtering data coming to the plugins e.g. // based on what it tells us upon registration, we might need to // filter `queued_output' as well. str_append_str (&plugin->write_buffer, &plugin->queued_output); str_free (&plugin->queued_output); // NOTE: this may trigger the buffer length check plugin_queue_write (plugin); } else if (!strcasecmp (command, "get_config")) { if (msg->params.len < 2) return; const char *value = str_map_find (&ctx->config, msg->params.vector[1]); // TODO: escape the value (although there's no need to ATM) plugin_send (plugin, "%s :%s", plugin_ipc_command, value ? value : ""); } else if (!strcasecmp (command, "print")) { if (msg->params.len < 2) return; printf ("%s\n", msg->params.vector[1]); } } else if (plugin->initialized && ctx->irc_ready) { // Pass everything else through to the IRC server // XXX: when the server isn't ready yet, these messages get silently // discarded, which shouldn't pose a problem most of the time. // Perhaps we could send a "connected" notification on `register' // if `irc_ready' is true, or after it becomes true later, so that // plugins know when to start sending unprovoked IRC messages. // XXX: another case is when the connection gets interrupted and the // plugin tries to send something back while we're reconnecting. // For that we might set up a global buffer that gets flushed out // after `irc_ready' becomes true. Note that there is always some // chance of messages getting lost without us even noticing it. irc_send (ctx, "%s", raw); } } static void on_plugin_readable (const struct pollfd *fd, struct plugin_data *plugin) { if (fd->revents & ~(POLLIN | POLLHUP | POLLERR)) print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents); // TODO: see if I can reuse irc_fill_read_buffer() struct str *buf = &plugin->read_buffer; while (true) { str_ensure_space (buf, 512 + 1); ssize_t n_read = read (fd->fd, buf->str + buf->len, buf->alloc - buf->len - 1); if (n_read < 0) { if (errno == EAGAIN) break; if (soft_assert (errno == EINTR)) continue; if (!plugin->is_zombie) { print_error ("failure on reading from plugin `%s'," " therefore I'm unloading it", plugin->name); plugin_zombify (plugin); } return; } // EOF; hopefully it will die soon (maybe it already has) if (n_read == 0) break; buf->str[buf->len += n_read] = '\0'; if (buf->len >= (1 << 20)) { // XXX: this isn't really the best flood prevention mechanism, // but it wasn't even supposed to be one. if (plugin->is_zombie) { print_error ("a zombie of plugin `%s' is trying to flood us," " therefore I'm killing it", plugin->name); kill (plugin->pid, SIGKILL); } else { print_error ("plugin `%s' seems to spew out data frantically," " therefore I'm unloading it", plugin->name); plugin_zombify (plugin); } return; } } irc_process_buffer (buf, plugin_process_message, plugin); } static bool is_valid_plugin_name (const char *name) { if (!*name) return false; for (const char *p = name; *p; p++) if (!isgraph (*p) || *p == '/') return false; return true; } static bool plugin_load (struct bot_context *ctx, const char *name, struct error **e) { const char *plugin_dir = str_map_find (&ctx->config, "plugin_dir"); if (!plugin_dir) { error_set (e, "plugin directory not set"); return false; } if (!is_valid_plugin_name (name)) { error_set (e, "invalid plugin name"); return false; } if (str_map_find (&ctx->plugins_by_name, name)) { error_set (e, "the plugin has already been loaded"); return false; } int stdin_pipe[2]; if (pipe (stdin_pipe) == -1) { error_set (e, "%s: %s: %s", "failed to load the plugin", "pipe", strerror (errno)); goto fail_1; } int stdout_pipe[2]; if (pipe (stdout_pipe) == -1) { error_set (e, "%s: %s: %s", "failed to load the plugin", "pipe", strerror (errno)); goto fail_2; } set_cloexec (stdin_pipe[1]); set_cloexec (stdout_pipe[0]); pid_t pid = fork (); if (pid == -1) { error_set (e, "%s: %s: %s", "failed to load the plugin", "fork", strerror (errno)); goto fail_3; } if (pid == 0) { // Redirect the child's stdin and stdout to the pipes hard_assert (dup2 (stdin_pipe[0], STDIN_FILENO) != -1); hard_assert (dup2 (stdout_pipe[1], STDOUT_FILENO) != -1); xclose (stdin_pipe[0]); xclose (stdout_pipe[1]); struct str pathname; str_init (&pathname); str_append (&pathname, plugin_dir); str_append_c (&pathname, '/'); str_append (&pathname, name); // Restore some of the signal handling signal (SIGPIPE, SIG_DFL); char *const argv[] = { pathname.str, NULL }; execve (argv[0], argv, environ); // We will collect the failure later via SIGCHLD print_error ("%s: %s: %s", "failed to load the plugin", "exec", strerror (errno)); _exit (EXIT_FAILURE); } xclose (stdin_pipe[0]); xclose (stdout_pipe[1]); set_blocking (stdout_pipe[0], false); set_blocking (stdin_pipe[1], false); struct plugin_data *plugin = xmalloc (sizeof *plugin); plugin_data_init (plugin); plugin->ctx = ctx; plugin->pid = pid; plugin->name = xstrdup (name); plugin->read_fd = stdout_pipe[0]; plugin->write_fd = stdin_pipe[1]; LIST_PREPEND (ctx->plugins, plugin); str_map_set (&ctx->plugins_by_name, name, plugin); poller_set (&ctx->poller, stdout_pipe[0], POLLIN, (poller_dispatcher_func) on_plugin_readable, plugin); return true; fail_3: xclose (stdout_pipe[0]); xclose (stdout_pipe[1]); fail_2: xclose (stdin_pipe[0]); xclose (stdin_pipe[1]); fail_1: return false; } static bool plugin_unload (struct bot_context *ctx, const char *name, struct error **e) { struct plugin_data *plugin = str_map_find (&ctx->plugins_by_name, name); if (!plugin) { error_set (e, "no such plugin is loaded"); return false; } plugin_zombify (plugin); // TODO: add a `kill zombies' command to forcefully get rid of processes // that do not understand the request. // TODO: set a timeout before we go for a kill automatically (and if this // was a reload request, try to bring the plugin back up) return true; } static void plugin_load_all_from_config (struct bot_context *ctx) { const char *plugin_list = str_map_find (&ctx->config, "plugins"); if (!plugin_list) return; struct str_vector plugins; str_vector_init (&plugins); split_str_ignore_empty (plugin_list, ',', &plugins); for (size_t i = 0; i < plugins.len; i++) { char *name = strip_str_in_place (plugins.vector[i], " "); struct error *e = NULL; if (!plugin_load (ctx, name, &e)) { print_error ("plugin `%s' failed to load: %s", name, e->message); error_free (e); } } str_vector_free (&plugins); } // --- Main program ------------------------------------------------------------ static bool parse_bot_command (const char *s, const char *command, const char **following) { size_t command_len = strlen (command); if (strncasecmp (s, command, command_len)) return false; s += command_len; // Expect a word boundary, so that we don't respond to invalid things if (isalnum (*s)) return false; // Ignore any initial spaces; the rest is the command's argument while (isblank (*s)) s++; *following = s; return true; } static void split_bot_command_argument_list (const char *arguments, struct str_vector *out) { split_str_ignore_empty (arguments, ',', out); for (size_t i = 0; i < out->len; ) { if (!*strip_str_in_place (out->vector[i], " \t")) str_vector_remove (out, i); else i++; } } static bool is_private_message (const struct irc_message *msg) { hard_assert (msg->params.len); return !strchr ("#&+!", *msg->params.vector[0]); } static bool is_sent_by_admin (struct bot_context *ctx, const struct irc_message *msg) { // No administrator set -> everyone is an administrator if (!ctx->admin_re) return true; return regexec (ctx->admin_re, msg->prefix, 0, NULL, 0) != REG_NOMATCH; } static void respond_to_user (struct bot_context *ctx, const struct irc_message *msg, const char *format, ...) ATTRIBUTE_PRINTF (3, 4); static void respond_to_user (struct bot_context *ctx, const struct irc_message *msg, const char *format, ...) { if (!soft_assert (msg->prefix && msg->params.len)) return; char nick[strcspn (msg->prefix, "!") + 1]; strncpy (nick, msg->prefix, sizeof nick - 1); nick[sizeof nick - 1] = '\0'; struct str text; va_list ap; str_init (&text); va_start (ap, format); str_append_vprintf (&text, format, ap); va_end (ap); if (is_private_message (msg)) irc_send (ctx, "PRIVMSG %s :%s", nick, text.str); else irc_send (ctx, "PRIVMSG %s :%s: %s", msg->params.vector[0], nick, text.str); str_free (&text); } static void process_plugin_load (struct bot_context *ctx, const struct irc_message *msg, const char *name) { struct error *e = NULL; if (plugin_load (ctx, name, &e)) respond_to_user (ctx, msg, "plugin `%s' queued for loading", name); else { respond_to_user (ctx, msg, "plugin `%s' could not be loaded: %s", name, e->message); error_free (e); } } static void process_plugin_unload (struct bot_context *ctx, const struct irc_message *msg, const char *name) { struct error *e = NULL; if (plugin_unload (ctx, name, &e)) respond_to_user (ctx, msg, "plugin `%s' unloaded", name); else { respond_to_user (ctx, msg, "plugin `%s' could not be unloaded: %s", name, e->message); error_free (e); } } static void process_plugin_reload (struct bot_context *ctx, const struct irc_message *msg, const char *name) { // So far the only error that can occur is that the plugin hasn't been // loaded, which in this case doesn't really matter. plugin_unload (ctx, name, NULL); process_plugin_load (ctx, msg, name); } static void process_privmsg (struct bot_context *ctx, const struct irc_message *msg) { if (!is_sent_by_admin (ctx, msg)) return; if (msg->params.len < 2) return; const char *prefix = str_map_find (&ctx->config, "prefix"); hard_assert (prefix != NULL); // We have a default value for this // For us to recognize the command, it has to start with the prefix, // with the exception of PM's sent directly to us. const char *text = msg->params.vector[1]; if (!strncmp (text, prefix, strlen (prefix))) text += strlen (prefix); else if (!is_private_message (msg)) return; const char *following; struct str_vector list; str_vector_init (&list); if (parse_bot_command (text, "quote", &following)) // This seems to replace tons of random stupid commands irc_send (ctx, "%s", following); else if (parse_bot_command (text, "quit", &following)) { // We actually need this command (instead of just `quote') because we // could try to reconnect to the server automatically otherwise. if (*following) irc_send (ctx, "QUIT :%s", following); else irc_send (ctx, "QUIT"); initiate_quit (ctx); } else if (parse_bot_command (text, "status", &following)) { struct str report; str_init (&report); const char *reason = getenv (g_startup_reason_str); if (!reason) reason = "launched normally"; str_append_printf (&report, "\x02startup reason:\x0f %s, ", reason); str_append (&report, "\x02plugins:\x0f "); size_t zombies = 0; for (struct plugin_data *plugin = ctx->plugins; plugin; plugin = plugin->next) { if (plugin->is_zombie) zombies++; else str_append_printf (&report, "%s, ", plugin->name); } if (!ctx->plugins) str_append (&report, "\x02none\x0f, "); str_append_printf (&report, "\x02zombies:\x0f %zu", zombies); respond_to_user (ctx, msg, "%s", report.str); str_free (&report); } else if (parse_bot_command (text, "load", &following)) { split_bot_command_argument_list (following, &list); for (size_t i = 0; i < list.len; i++) process_plugin_load (ctx, msg, list.vector[i]); } else if (parse_bot_command (text, "reload", &following)) { split_bot_command_argument_list (following, &list); for (size_t i = 0; i < list.len; i++) process_plugin_reload (ctx, msg, list.vector[i]); } else if (parse_bot_command (text, "unload", &following)) { split_bot_command_argument_list (following, &list); for (size_t i = 0; i < list.len; i++) process_plugin_unload (ctx, msg, list.vector[i]); } str_vector_free (&list); } static void irc_forward_message_to_plugins (struct bot_context *ctx, const char *raw) { // For consistency with plugin_process_message() if (!ctx->irc_ready) return; for (struct plugin_data *plugin = ctx->plugins; plugin; plugin = plugin->next) { if (plugin->is_zombie) continue; if (plugin->initialized) plugin_send (plugin, "%s", raw); else // TODO: make sure that this buffer doesn't get too large either str_append_printf (&plugin->queued_output, "%s\r\n", raw); } } static void irc_process_message (const struct irc_message *msg, const char *raw, void *user_data) { struct bot_context *ctx = user_data; if (g_debug_mode) fprintf (stderr, "[%s] ==> \"%s\"\n", "IRC", raw); // This should be as minimal as possible, I don't want to have the whole bot // written in C, especially when I have this overengineered plugin system. // Therefore the very basic functionality only. // // I should probably even rip out the autojoin... irc_forward_message_to_plugins (ctx, raw); if (!strcasecmp (msg->command, "PING")) { if (msg->params.len) irc_send (ctx, "PONG :%s", msg->params.vector[0]); else irc_send (ctx, "PONG"); } else if (!ctx->irc_ready && (!strcasecmp (msg->command, "MODE") || !strcasecmp (msg->command, "376") // RPL_ENDOFMOTD || !strcasecmp (msg->command, "422"))) // ERR_NOMOTD { print_status ("successfully connected"); ctx->irc_ready = true; const char *autojoin = str_map_find (&ctx->config, "autojoin"); if (autojoin) irc_send (ctx, "JOIN :%s", autojoin); } else if (!strcasecmp (msg->command, "PRIVMSG")) process_privmsg (ctx, msg); } enum irc_read_result { IRC_READ_OK, ///< Some data were read successfully IRC_READ_EOF, ///< The server has closed connection IRC_READ_AGAIN, ///< No more data at the moment IRC_READ_ERROR ///< General connection failure }; static enum irc_read_result irc_fill_read_buffer_ssl (struct bot_context *ctx, struct str *buf) { int n_read; start: n_read = SSL_read (ctx->ssl, buf->str + buf->len, buf->alloc - buf->len - 1 /* null byte */); const char *error_info = NULL; switch (xssl_get_error (ctx->ssl, n_read, &error_info)) { case SSL_ERROR_NONE: buf->str[buf->len += n_read] = '\0'; return IRC_READ_OK; case SSL_ERROR_ZERO_RETURN: return IRC_READ_EOF; case SSL_ERROR_WANT_READ: return IRC_READ_AGAIN; case SSL_ERROR_WANT_WRITE: { // Let it finish the handshake as we don't poll for writability; // any errors are to be collected by SSL_read() in the next iteration struct pollfd pfd = { .fd = ctx->irc_fd, .events = POLLOUT }; soft_assert (poll (&pfd, 1, 0) > 0); goto start; } case XSSL_ERROR_TRY_AGAIN: goto start; default: print_debug ("%s: %s: %s", __func__, "SSL_read", error_info); return IRC_READ_ERROR; } } static enum irc_read_result irc_fill_read_buffer (struct bot_context *ctx, struct str *buf) { ssize_t n_read; start: n_read = recv (ctx->irc_fd, buf->str + buf->len, buf->alloc - buf->len - 1 /* null byte */, 0); if (n_read > 0) { buf->str[buf->len += n_read] = '\0'; return IRC_READ_OK; } if (n_read == 0) return IRC_READ_EOF; if (errno == EAGAIN) return IRC_READ_AGAIN; if (errno == EINTR) goto start; print_debug ("%s: %s: %s", __func__, "recv", strerror (errno)); return IRC_READ_ERROR; } static bool irc_connect (struct bot_context *ctx, struct error **e); static void irc_try_reconnect (struct bot_context *ctx) { if (!soft_assert (ctx->irc_fd == -1)) return; const char *reconnect_str = str_map_find (&ctx->config, "reconnect"); hard_assert (reconnect_str != NULL); // We have a default value for this bool reconnect; if (!set_boolean_if_valid (&reconnect, reconnect_str)) { print_fatal ("invalid configuration value for `%s'", "recover"); try_finish_quit (ctx); return; } if (!reconnect) return; const char *delay_str = str_map_find (&ctx->config, "reconnect_delay"); hard_assert (delay_str != NULL); // We have a default value for this unsigned long delay; if (!xstrtoul (&delay, delay_str, 10)) { print_error ("invalid configuration value for `%s'", "reconnect_delay"); delay = 0; } while (true) { // TODO: this would be better suited by a timeout event; // remember to update try_finish_quit() etc. to reflect this print_status ("trying to reconnect in %ld seconds...", delay); sleep (delay); struct error *e = NULL; if (irc_connect (ctx, &e)) break; print_error ("%s", e->message); error_free (e); } // TODO: inform plugins about the new connection } static void on_irc_disconnected (struct bot_context *ctx) { // Get rid of the dead socket etc. if (ctx->ssl) { SSL_free (ctx->ssl); ctx->ssl = NULL; SSL_CTX_free (ctx->ssl_ctx); ctx->ssl_ctx = NULL; } ssize_t i = poller_find_by_fd (&ctx->poller, ctx->irc_fd); if (i != -1) poller_remove_at_index (&ctx->poller, i); xclose (ctx->irc_fd); ctx->irc_fd = -1; ctx->irc_ready = false; // TODO: inform plugins about the disconnect event if (ctx->quitting) { // Unload all plugins // TODO: wait for a few seconds and then send SIGKILL to all plugins for (struct plugin_data *plugin = ctx->plugins; plugin; plugin = plugin->next) plugin_zombify (plugin); try_finish_quit (ctx); return; } irc_try_reconnect (ctx); } static void on_irc_readable (const struct pollfd *fd, struct bot_context *ctx) { if (fd->revents & ~(POLLIN | POLLHUP | POLLERR)) print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents); (void) set_blocking (ctx->irc_fd, false); struct str *buf = &ctx->read_buffer; enum irc_read_result (*fill_buffer)(struct bot_context *, struct str *) = ctx->ssl ? irc_fill_read_buffer_ssl : irc_fill_read_buffer; bool disconnected = false; while (true) { str_ensure_space (buf, 512); switch (fill_buffer (ctx, buf)) { case IRC_READ_AGAIN: goto end; case IRC_READ_ERROR: print_error ("reading from the IRC server failed"); disconnected = true; goto end; case IRC_READ_EOF: print_status ("the IRC server closed the connection"); disconnected = true; goto end; case IRC_READ_OK: break; } if (buf->len >= (1 << 20)) { print_error ("the IRC server seems to spew out data frantically"); irc_shutdown (ctx); goto end; } } end: (void) set_blocking (ctx->irc_fd, true); irc_process_buffer (buf, irc_process_message, ctx); if (disconnected) on_irc_disconnected (ctx); } static bool irc_connect (struct bot_context *ctx, struct error **e) { const char *irc_host = str_map_find (&ctx->config, "irc_host"); const char *irc_port = str_map_find (&ctx->config, "irc_port"); const char *ssl_use_str = str_map_find (&ctx->config, "ssl_use"); const char *nickname = str_map_find (&ctx->config, "nickname"); const char *username = str_map_find (&ctx->config, "username"); const char *realname = str_map_find (&ctx->config, "realname"); // We have a default value for these hard_assert (irc_port && ssl_use_str); hard_assert (nickname && username && realname); // TODO: again, get rid of `struct error' in here. The question is: how // do we tell our caller that he should not try to reconnect? if (!irc_host) { error_set (e, "no hostname specified in configuration"); return false; } bool use_ssl; if (!set_boolean_if_valid (&use_ssl, ssl_use_str)) { error_set (e, "invalid configuration value for `%s'", "use_ssl"); return false; } if (!irc_establish_connection (ctx, irc_host, irc_port, use_ssl, e)) return false; // TODO: set a timeout on the socket, something like 30 minutes, then we // should ideally send a PING... or just forcefully reconnect. // // TODO: in exec try: 1/ set blocking, 2/ setsockopt() SO_LINGER, // (struct linger) { .l_onoff = true; .l_linger = 1 /* 1s should do */; } // 3/ /* O_CLOEXEC */ But only if the QUIT message proves unreliable. poller_set (&ctx->poller, ctx->irc_fd, POLLIN, (poller_dispatcher_func) on_irc_readable, ctx); // TODO: probably check for errors from these calls as well irc_send (ctx, "NICK %s", nickname); irc_send (ctx, "USER %s 8 * :%s", username, realname); return true; } static bool load_admin_regex (struct bot_context *ctx) { hard_assert (!ctx->admin_re); const char *admin = str_map_find (&ctx->config, "admin"); if (!admin) return true; struct error *e = NULL; ctx->admin_re = regex_compile (admin, REG_EXTENDED | REG_NOSUB, &e); if (!e) return true; print_error ("invalid configuration value for `%s': %s", "admin", e->message); error_free (e); return false; } static void on_signal_pipe_readable (const struct pollfd *fd, struct bot_context *ctx) { char *dummy; (void) read (fd->fd, &dummy, 1); // XXX: do we need to check if we have a connection? if (g_termination_requested && !ctx->quitting) { irc_send (ctx, "QUIT :Terminated by signal"); initiate_quit (ctx); } // Reap all dead children (since the pipe may overflow, we ask waitpid() // to return all the zombies it knows about). while (true) { int status; pid_t zombie = waitpid (-1, &status, WNOHANG); if (zombie == -1) { // No children to wait on if (errno == ECHILD) break; hard_assert (errno == EINTR); continue; } if (zombie == 0) break; struct plugin_data *plugin = plugin_find_by_pid (ctx, zombie); // Something has died but we don't recognize it (re-exec?) if (!soft_assert (plugin != NULL)) continue; // TODO: callbacks on children death, so that we may tell the user // "plugin `name' died like a dirty jewish pig"; use `status' if (!plugin->is_zombie && WIFSIGNALED (status)) { const char *notes = ""; #ifdef WCOREDUMP if (WCOREDUMP (status)) notes = " (core dumped)"; #endif print_warning ("Plugin `%s' died from signal %d%s", plugin->name, WTERMSIG (status), notes); } // Let's go through the zombie state to simplify things a bit // TODO: might not be a completely bad idea to restart the plugin plugin_zombify (plugin); plugin->pid = -1; ssize_t poller_idx = poller_find_by_fd (&ctx->poller, plugin->read_fd); if (poller_idx != -1) poller_remove_at_index (&ctx->poller, poller_idx); xclose (plugin->read_fd); plugin->read_fd = -1; LIST_UNLINK (ctx->plugins, plugin); plugin_data_free (plugin); free (plugin); // Living child processes block us from quitting try_finish_quit (ctx); } } static void print_usage (const char *program_name) { fprintf (stderr, "Usage: %s [OPTION]...\n" "Experimental IRC bot.\n" "\n" " -d, --debug run in debug mode\n" " -h, --help display this help and exit\n" " -V, --version output version information and exit\n" " --write-default-cfg [filename]\n" " write a default configuration file and exit\n", program_name); } int main (int argc, char *argv[]) { const char *invocation_name = argv[0]; str_vector_init (&g_original_argv); str_vector_add_vector (&g_original_argv, argv); static struct option opts[] = { { "debug", no_argument, NULL, 'd' }, { "help", no_argument, NULL, 'h' }, { "version", no_argument, NULL, 'V' }, { "write-default-cfg", optional_argument, NULL, 'w' }, { NULL, 0, NULL, 0 } }; while (1) { int c, opt_index; c = getopt_long (argc, argv, "dhV", opts, &opt_index); if (c == -1) break; switch (c) { case 'd': g_debug_mode = true; break; case 'h': print_usage (invocation_name); exit (EXIT_SUCCESS); case 'V': printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); exit (EXIT_SUCCESS); case 'w': call_write_default_config (optarg, g_config_table); exit (EXIT_SUCCESS); default: print_error ("wrong options"); exit (EXIT_FAILURE); } } print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting"); setup_signal_handlers (); SSL_library_init (); atexit (EVP_cleanup); SSL_load_error_strings (); // XXX: ERR_load_BIO_strings()? Anything else? atexit (ERR_free_strings); struct bot_context ctx; bot_context_init (&ctx); struct error *e = NULL; if (!read_config_file (&ctx.config, &e)) { print_error ("error loading configuration: %s", e->message); error_free (e); exit (EXIT_FAILURE); } setup_recovery_handler (&ctx); poller_set (&ctx.poller, g_signal_pipe[0], POLLIN, (poller_dispatcher_func) on_signal_pipe_readable, &ctx); plugin_load_all_from_config (&ctx); if (!load_admin_regex (&ctx)) exit (EXIT_FAILURE); if (!irc_connect (&ctx, &e)) { print_error ("%s", e->message); error_free (e); exit (EXIT_FAILURE); } // TODO: clean re-exec support; to save the state I can either use argv, // argp, or I can create a temporary file, unlink it and use the FD // (mkstemp() on a `struct str' constructed from XDG_RUNTIME_DIR, TMPDIR // or /tmp as a last resort + PROGRAM_NAME + ".XXXXXX" -> unlink(); // remember to use O_CREAT | O_EXCL). The state needs to be versioned. // Unfortunately I cannot de/serialize SSL state. ctx.polling = true; while (ctx.polling) poller_run (&ctx.poller); bot_context_free (&ctx); str_vector_free (&g_original_argv); return EXIT_SUCCESS; }