/* * zyklonb.c: the experimental IRC bot * * Copyright (c) 2014 - 2016, Přemysl Janouch
* * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted. * * 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. * */ #include "config.h" #define PROGRAM_NAME "ZyklonB" #define PLUGIN_DIR ZYKLONB_PLUGIN_DIR #include "common.c" // --- Configuration (application-specific) ------------------------------------ static struct simple_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" }, { "tls", "off", "Whether to use TLS" }, { "tls_cert", NULL, "Client TLS certificate (PEM)" }, { "tls_verify", "on", "Whether to verify certificates" }, { "tls_ca_file", NULL, "OpenSSL CA bundle file" }, { "tls_ca_path", NULL, "OpenSSL CA bundle path" }, { "autojoin", NULL, "Channels to join on start" }, { "reconnect", "on", "Whether to reconnect on error" }, { "reconnect_delay", "5", "Time between reconnecting" }, { "socks_host", NULL, "Address of a SOCKS 4a/5 proxy" }, { "socks_port", "1080", "SOCKS port number" }, { "socks_username", NULL, "SOCKS auth. username" }, { "socks_password", NULL, "SOCKS auth. password" }, { "prefix", ":", "The prefix for bot commands" }, { "admin", NULL, "Host mask for administrators" }, { "plugins", NULL, "The plugins to load on startup" }, { "plugin_dir", PLUGIN_DIR, "Where to search for plugins" }, { "recover", "on", "Whether to re-launch on crash" }, { NULL, NULL, NULL } }; // --- Application data -------------------------------------------------------- struct plugin { LIST_HEADER (struct plugin) 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 poller_fd read_event; ///< Read FD event struct poller_fd write_event; ///< Write FD event struct str read_buffer; ///< Unprocessed input struct str write_buffer; ///< Output yet to be sent out }; static struct plugin * plugin_new (void) { struct plugin *self = xcalloc (1, sizeof *self); self->pid = -1; self->queued_output = str_make (); self->read_fd = -1; self->read_buffer = str_make (); self->write_fd = -1; self->write_buffer = str_make (); return self; } static void plugin_destroy (struct plugin *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); free (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct bot_context { struct str_map config; ///< User configuration regex_t *admin_re; ///< Regex to match our administrator bool reconnect; ///< Whether to reconnect on conn. fail. unsigned long reconnect_delay; ///< Reconnect delay in seconds int irc_fd; ///< Socket FD of the server struct str read_buffer; ///< Input yet to be processed struct poller_fd irc_event; ///< IRC FD event bool irc_registered; ///< Whether we may send messages now struct poller_fd signal_event; ///< Signal FD event struct poller_timer ping_tmr; ///< We should send a ping struct poller_timer timeout_tmr; ///< Connection seems to be dead struct poller_timer reconnect_tmr; ///< We should reconnect now SSL_CTX *ssl_ctx; ///< SSL context SSL *ssl; ///< SSL connection struct plugin *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 on_irc_ping_timeout (void *user_data); static void on_irc_timeout (void *user_data); static void on_irc_reconnect_timeout (void *user_data); static void bot_context_init (struct bot_context *self) { self->config = str_map_make (free); simple_config_load_defaults (&self->config, g_config_table); self->admin_re = NULL; self->irc_fd = -1; self->read_buffer = str_make (); self->irc_registered = false; self->ssl = NULL; self->ssl_ctx = NULL; self->plugins = NULL; self->plugins_by_name = str_map_make (NULL); poller_init (&self->poller); self->quitting = false; self->polling = false; self->timeout_tmr = poller_timer_make (&self->poller); self->timeout_tmr.dispatcher = on_irc_timeout; self->timeout_tmr.user_data = self; self->ping_tmr = poller_timer_make (&self->poller); self->ping_tmr.dispatcher = on_irc_ping_timeout; self->ping_tmr.user_data = self; self->reconnect_tmr = poller_timer_make (&self->poller); self->reconnect_tmr.dispatcher = on_irc_reconnect_timeout; self->reconnect_tmr.user_data = self; } 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 LIST_FOR_EACH (struct plugin, link, self->plugins) plugin_destroy (link); if (self->irc_fd != -1) { poller_fd_reset (&self->irc_event); 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) { // TODO: set a timer after which we cut the connection? // 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 try_finish_quit (struct bot_context *ctx) { if (ctx->quitting && ctx->irc_fd == -1 && !ctx->plugins) ctx->polling = false; } static bool plugin_zombify (struct plugin *); static void initiate_quit (struct bot_context *ctx) { // Initiate bringing down of the two things that block our shutdown: // a/ the IRC socket, b/ our child processes: for (struct plugin *plugin = ctx->plugins; plugin; plugin = plugin->next) plugin_zombify (plugin); if (ctx->irc_fd != -1) irc_shutdown (ctx); ctx->quitting = true; try_finish_quit (ctx); } static bool irc_send (struct bot_context *ctx, const char *format, ...) ATTRIBUTE_PRINTF (2, 3); 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_make (); 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 ERR_clear_error (); 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_get_boolean_from_config (struct bot_context *ctx, const char *name, bool *value, struct error **e) { const char *str = str_map_find (&ctx->config, name); hard_assert (str != NULL); if (set_boolean_if_valid (value, str)) return true; return error_set (e, "invalid configuration value for `%s'", name); } static bool irc_initialize_ca_set (SSL_CTX *ssl_ctx, const char *file, const char *path, struct error **e) { ERR_clear_error (); if (file || path) { if (SSL_CTX_load_verify_locations (ssl_ctx, file, path)) return true; return error_set (e, "%s: %s", "failed to set locations for the CA certificate bundle", ERR_reason_error_string (ERR_get_error ())); } if (!SSL_CTX_set_default_verify_paths (ssl_ctx)) return error_set (e, "%s: %s", "couldn't load the default CA certificate bundle", ERR_reason_error_string (ERR_get_error ())); return true; } static bool irc_initialize_ca (struct bot_context *ctx, struct error **e) { const char *ca_file = str_map_find (&ctx->config, "tls_ca_file"); const char *ca_path = str_map_find (&ctx->config, "tls_ca_path"); char *full_file = ca_file ? resolve_filename (ca_file, resolve_relative_config_filename) : NULL; char *full_path = ca_path ? resolve_filename (ca_path, resolve_relative_config_filename) : NULL; bool ok = false; if (ca_file && !full_file) error_set (e, "couldn't find the CA bundle file"); else if (ca_path && !full_path) error_set (e, "couldn't find the CA bundle path"); else ok = irc_initialize_ca_set (ctx->ssl_ctx, full_file, full_path, e); free (full_file); free (full_path); return ok; } static bool irc_initialize_ssl_ctx (struct bot_context *ctx, struct error **e) { // Disable deprecated protocols (see RFC 7568) SSL_CTX_set_options (ctx->ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); bool verify; if (!irc_get_boolean_from_config (ctx, "tls_verify", &verify, e)) return false; SSL_CTX_set_verify (ctx->ssl_ctx, verify ? SSL_VERIFY_PEER : SSL_VERIFY_NONE, NULL); struct error *error = NULL; if (!irc_initialize_ca (ctx, &error)) { if (verify) { error_propagate (e, error); return false; } // Only inform the user if we're not actually verifying print_warning ("%s", error->message); error_free (error); } return true; } static bool irc_initialize_tls (struct bot_context *ctx, struct error **e) { const char *error_info = NULL; ctx->ssl_ctx = SSL_CTX_new (SSLv23_client_method ()); if (!ctx->ssl_ctx) goto error_ssl_1; if (!irc_initialize_ssl_ctx (ctx, e)) goto error_ssl_2; ctx->ssl = SSL_new (ctx->ssl_ctx); if (!ctx->ssl) goto error_ssl_2; const char *tls_cert = str_map_find (&ctx->config, "tls_cert"); if (tls_cert) { char *path = resolve_filename (tls_cert, resolve_relative_config_filename); if (!path) print_error ("%s: %s", "cannot open file", tls_cert); // XXX: perhaps we should read the file ourselves for better messages else if (!SSL_use_certificate_file (ctx->ssl, path, SSL_FILETYPE_PEM) || !SSL_use_PrivateKey_file (ctx->ssl, path, SSL_FILETYPE_PEM)) print_error ("%s: %s", "setting the TLS 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); switch (xssl_get_error (ctx->ssl, SSL_connect (ctx->ssl), &error_info)) { case SSL_ERROR_NONE: return true; case SSL_ERROR_ZERO_RETURN: error_info = "server closed the connection"; default: break; } 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. if (!error_info) error_info = ERR_error_string (ERR_get_error (), NULL); return error_set (e, "%s: %s", "could not initialize TLS", error_info); } static bool irc_establish_connection (struct bot_context *ctx, const char *host, const char *port, struct error **e) { struct addrinfo gai_hints, *gai_result, *gai_iter; memset (&gai_hints, 0, sizeof gai_hints); gai_hints.ai_socktype = SOCK_STREAM; int err = getaddrinfo (host, port, &gai_hints, &gai_result); if (err) return error_set (e, "%s: %s: %s", "connection failed", "getaddrinfo", gai_strerror (err)); 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! char *address = format_host_port_pair (real_host, port); print_status ("connecting to %s...", address); free (address); if (!connect (sockfd, gai_iter->ai_addr, gai_iter->ai_addrlen)) break; xclose (sockfd); } freeaddrinfo (gai_result); if (!gai_iter) return error_set (e, "connection failed"); ctx->irc_fd = sockfd; return true; } // --- Signals ----------------------------------------------------------------- static int g_signal_pipe[2]; ///< A pipe used to signal... signals static struct strv 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; // Avoid annoying resource intensive infinite loops by sleeping for a bit (void) sleep (1); // 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) { g_recovery_env = strv_make (); strv_append_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 { g_startup_reason_location = g_recovery_env.vector + g_recovery_env.len; strv_append (&g_recovery_env, ""); } } static bool setup_recovery_handler (struct bot_context *ctx, struct error **e) { bool recover; if (!irc_get_boolean_from_config (ctx, "recover", &recover, e)) return false; if (!recover) return true; // 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)); return true; } // --- Plugins ----------------------------------------------------------------- /// The name of the special IRC command for interprocess communication static const char *plugin_ipc_command = "ZYKLONB"; static struct plugin * plugin_find_by_pid (struct bot_context *ctx, pid_t pid) { struct plugin *iter; for (iter = ctx->plugins; iter; iter = iter->next) if (iter->pid == pid) return iter; return NULL; } static bool plugin_zombify (struct plugin *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). poller_fd_reset (&plugin->write_event); // 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; // TODO: wait a few seconds and then send SIGKILL to the plugin return true; } static void on_plugin_writable (const struct pollfd *fd, struct plugin *plugin) { 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 poller_fd_reset (&plugin->write_event); } static void plugin_queue_write (struct plugin *plugin) { if (plugin->is_zombie) return; // Don't let the write buffer grow indefinitely. 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_fd_set (&plugin->write_event, POLLOUT); } static void plugin_send (struct plugin *plugin, const char *format, ...) ATTRIBUTE_PRINTF (2, 3); static void plugin_send (struct plugin *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_ipc (struct plugin *plugin, const struct irc_message *msg) { // 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 (&plugin->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]); } } static void plugin_process_message (const struct irc_message *msg, const char *raw, void *user_data) { struct plugin *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)) plugin_process_ipc (plugin, msg); else if (plugin->initialized && ctx->irc_registered) { // 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 *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_reserve (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 struct plugin * plugin_launch (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 NULL; } int stdin_pipe[2]; if (pipe (stdin_pipe) == -1) { error_set (e, "%s: %s", "pipe", strerror (errno)); return NULL; } int stdout_pipe[2]; if (pipe (stdout_pipe) == -1) { error_set (e, "%s: %s", "pipe", strerror (errno)); goto fail_1; } struct str work_dir = str_make (); get_xdg_home_dir (&work_dir, "XDG_DATA_HOME", ".local/share"); str_append_printf (&work_dir, "/%s", PROGRAM_NAME); if (!mkdir_with_parents (work_dir.str, e)) 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", "fork", strerror (errno)); goto fail_2; } if (pid == 0) { // Redirect the child's stdin and stdout to the pipes if (dup2 (stdin_pipe[0], STDIN_FILENO) == -1 || dup2 (stdout_pipe[1], STDOUT_FILENO) == -1) { print_error ("%s: %s: %s", "failed to load the plugin", "dup2", strerror (errno)); _exit (EXIT_FAILURE); } if (chdir (work_dir.str)) { print_error ("%s: %s: %s", "failed to load the plugin", "chdir", strerror (errno)); _exit (EXIT_FAILURE); } xclose (stdin_pipe[0]); xclose (stdout_pipe[1]); // Restore some of the signal handling signal (SIGPIPE, SIG_DFL); char *argv[] = { xstrdup_printf ("%s/%s", plugin_dir, name), 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); } str_free (&work_dir); xclose (stdin_pipe[0]); xclose (stdout_pipe[1]); struct plugin *plugin = plugin_new (); plugin->ctx = ctx; plugin->pid = pid; plugin->name = xstrdup (name); plugin->read_fd = stdout_pipe[0]; plugin->write_fd = stdin_pipe[1]; return plugin; fail_2: str_free (&work_dir); xclose (stdout_pipe[0]); xclose (stdout_pipe[1]); fail_1: xclose (stdin_pipe[0]); xclose (stdin_pipe[1]); return NULL; } static bool plugin_load (struct bot_context *ctx, const char *name, struct error **e) { if (!is_valid_plugin_name (name)) return error_set (e, "invalid plugin name"); if (str_map_find (&ctx->plugins_by_name, name)) return error_set (e, "the plugin has already been loaded"); struct plugin *plugin; if (!(plugin = plugin_launch (ctx, name, e))) return false; set_blocking (plugin->read_fd, false); set_blocking (plugin->write_fd, false); plugin->read_event = poller_fd_make (&ctx->poller, plugin->read_fd); plugin->read_event.dispatcher = (poller_fd_fn) on_plugin_readable; plugin->read_event.user_data = plugin; plugin->write_event = poller_fd_make (&ctx->poller, plugin->write_fd); plugin->write_event.dispatcher = (poller_fd_fn) on_plugin_writable; plugin->write_event.user_data = plugin; LIST_PREPEND (ctx->plugins, plugin); str_map_set (&ctx->plugins_by_name, name, plugin); poller_fd_set (&plugin->read_event, POLLIN); return true; } static bool plugin_unload (struct bot_context *ctx, const char *name, struct error **e) { struct plugin *plugin = str_map_find (&ctx->plugins_by_name, name); if (!plugin) return error_set (e, "no such plugin is loaded"); plugin_zombify (plugin); // TODO: add a `kill zombies' command to forcefully get rid of processes // that do not understand the request. 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 strv plugins = strv_make (); cstr_split (plugin_list, ",", true, &plugins); for (size_t i = 0; i < plugins.len; i++) { char *name = cstr_strip_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); } } strv_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 strv *out) { cstr_split (arguments, ",", true, out); for (size_t i = 0; i < out->len; ) { if (!*cstr_strip_in_place (out->vector[i], " \t")) strv_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'; va_list ap; struct str text = str_make (); 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) { // XXX: we might want to wait until the plugin terminates before we try // to reload it (so that it can save its configuration or whatever) // 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 char * make_status_report (struct bot_context *ctx) { struct str report = str_make (); const char *reason = getenv (g_startup_reason_str); if (!reason) reason = "launched normally"; str_append_printf (&report, "\x02startup reason:\x0f %s", reason); size_t zombies = 0; const char *prepend = "; \x02plugins:\x0f "; for (struct plugin *plugin = ctx->plugins; plugin; plugin = plugin->next) { if (plugin->is_zombie) zombies++; else { str_append_printf (&report, "%s%s", prepend, plugin->name); prepend = ", "; } } if (!ctx->plugins) str_append_printf (&report, "%s\x02none\x0f", prepend); str_append_printf (&report, "; \x02zombies:\x0f %zu", zombies); return str_steal (&report); } 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 strv list = strv_make (); 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)) { char *report = make_status_report (ctx); respond_to_user (ctx, msg, "%s", report); 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]); } strv_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_registered) return; for (struct plugin *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_registered && !strcasecmp (msg->command, "001")) { print_status ("successfully connected"); ctx->irc_registered = 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_tls (struct bot_context *ctx, struct str *buf) { int n_read; start: ERR_clear_error (); 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 *, struct error **); static void irc_queue_reconnect (struct bot_context *); static void irc_cancel_timers (struct bot_context *ctx) { poller_timer_reset (&ctx->timeout_tmr); poller_timer_reset (&ctx->ping_tmr); poller_timer_reset (&ctx->reconnect_tmr); } static void on_irc_reconnect_timeout (void *user_data) { struct bot_context *ctx = user_data; struct error *e = NULL; if (irc_connect (ctx, &e)) { // TODO: inform plugins about the new connection return; } print_error ("%s", e->message); error_free (e); irc_queue_reconnect (ctx); } static void irc_queue_reconnect (struct bot_context *ctx) { hard_assert (ctx->irc_fd == -1); print_status ("trying to reconnect in %ld seconds...", ctx->reconnect_delay); poller_timer_set (&ctx->reconnect_tmr, ctx->reconnect_delay * 1000); } static void on_irc_disconnected (struct bot_context *ctx) { // Get rid of the dead socket and related things if (ctx->ssl) { SSL_free (ctx->ssl); ctx->ssl = NULL; SSL_CTX_free (ctx->ssl_ctx); ctx->ssl_ctx = NULL; } poller_fd_reset (&ctx->irc_event); xclose (ctx->irc_fd); ctx->irc_fd = -1; ctx->irc_registered = false; // TODO: inform plugins about the disconnect event // All of our timers have lost their meaning now irc_cancel_timers (ctx); if (ctx->quitting) try_finish_quit (ctx); else if (!ctx->reconnect) initiate_quit (ctx); else irc_queue_reconnect (ctx); } static void on_irc_ping_timeout (void *user_data) { struct bot_context *ctx = user_data; print_error ("connection timeout"); on_irc_disconnected (ctx); } static void on_irc_timeout (void *user_data) { // Provoke a response from the server struct bot_context *ctx = user_data; irc_send (ctx, "PING :%s", (char *) str_map_find (&ctx->config, "nickname")); } static void irc_reset_connection_timeouts (struct bot_context *ctx) { irc_cancel_timers (ctx); poller_timer_set (&ctx->timeout_tmr, 3 * 60 * 1000); poller_timer_set (&ctx->ping_tmr, (3 * 60 + 30) * 1000); } 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_tls : irc_fill_read_buffer; bool disconnected = false; while (true) { str_reserve (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); else irc_reset_connection_timeouts (ctx); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // The bot is currently mostly synchronous (which also makes it shorter), // however our current SOCKS code is not, hence we must wrap it. struct irc_socks_data { struct bot_context *ctx; ///< Bot context struct poller inner_poller; ///< Special inner poller bool polling; ///< Inner poller is no longer needed struct socks_connector connector; ///< SOCKS connector bool succeeded; ///< Were we successful in connecting? }; static void irc_on_socks_connected (void *user_data, int socket, const char *hostname) { (void) hostname; struct irc_socks_data *data = user_data; data->ctx->irc_fd = socket; data->succeeded = true; data->polling = false; } static void irc_on_socks_failure (void *user_data) { struct irc_socks_data *data = user_data; data->succeeded = false; data->polling = false; } static void irc_on_socks_connecting (void *user_data, const char *address, const char *via, const char *version) { (void) user_data; print_status ("connecting to %s via %s (%s)...", address, via, version); } static void irc_on_socks_error (void *user_data, const char *error) { (void) user_data; print_error ("%s: %s", "SOCKS connection failed", error); } static bool irc_establish_connection_socks (struct bot_context *ctx, const char *socks_host, const char *socks_port, const char *host, const char *service, struct error **e) { struct irc_socks_data data; struct poller *poller = &data.inner_poller; struct socks_connector *connector = &data.connector; data.ctx = ctx; poller_init (poller); data.polling = true; socks_connector_init (connector, poller); data.succeeded = false; connector->on_connected = irc_on_socks_connected; connector->on_connecting = irc_on_socks_connecting; connector->on_error = irc_on_socks_error; connector->on_failure = irc_on_socks_failure; connector->user_data = &data; if (socks_connector_add_target (connector, host, service, e)) { socks_connector_run (connector, socks_host, socks_port, str_map_find (&ctx->config, "socks_username"), str_map_find (&ctx->config, "socks_password")); while (data.polling) poller_run (poller); if (!data.succeeded) error_set (e, "connection failed"); } socks_connector_free (connector); poller_free (poller); return data.succeeded; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 *socks_host = str_map_find (&ctx->config, "socks_host"); const char *socks_port = str_map_find (&ctx->config, "socks_port"); 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 && socks_port); 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) return error_set (e, "no hostname specified in configuration"); bool use_tls; if (!irc_get_boolean_from_config (ctx, "tls", &use_tls, e)) return false; bool connected = socks_host ? irc_establish_connection_socks (ctx, socks_host, socks_port, irc_host, irc_port, e) : irc_establish_connection (ctx, irc_host, irc_port, e); if (!connected) return false; if (use_tls && !irc_initialize_tls (ctx, e)) { xclose (ctx->irc_fd); ctx->irc_fd = -1; return false; } print_status ("connection established"); ctx->irc_event = poller_fd_make (&ctx->poller, ctx->irc_fd); ctx->irc_event.dispatcher = (poller_fd_fn) on_irc_readable; ctx->irc_event.user_data = ctx; // 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_fd_set (&ctx->irc_event, POLLIN); irc_reset_connection_timeouts (ctx); irc_send (ctx, "NICK %s", nickname); irc_send (ctx, "USER %s 8 * :%s", username, realname); return true; } static bool parse_config (struct bot_context *ctx, struct error **e) { if (!irc_get_boolean_from_config (ctx, "reconnect", &ctx->reconnect, e)) return false; const char *delay_str = str_map_find (&ctx->config, "reconnect_delay"); hard_assert (delay_str != NULL); // We have a default value for this if (!xstrtoul (&ctx->reconnect_delay, delay_str, 10)) { return error_set (e, "invalid configuration value for `%s'", "reconnect_delay"); } hard_assert (!ctx->admin_re); const char *admin = str_map_find (&ctx->config, "admin"); if (!admin) return true; struct error *error = NULL; ctx->admin_re = regex_compile (admin, REG_EXTENDED | REG_NOSUB, &error); if (!error) return true; error_set (e, "invalid configuration value for `%s': %s", "admin", error->message); error_free (error); return false; } static void on_plugin_death (struct plugin *plugin, int status) { struct bot_context *ctx = plugin->ctx; // 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; // In theory we could close `read_fd', set `read_event->closed' to true // and expect epoll to no longer return events for the descriptor, as // all the pipe ends should be closed by then (the child is dead, so its // pipe FDs have been closed [assuming it hasn't forked without closing // the descriptors, which would be evil], and we would have closed all // of our FDs for this pipe as well). In practice that doesn't work. poller_fd_reset (&plugin->read_event); xclose (plugin->read_fd); plugin->read_fd = -1; LIST_UNLINK (ctx->plugins, plugin); plugin_destroy (plugin); // Living child processes block us from quitting try_finish_quit (ctx); } static bool try_reap_plugin (struct bot_context *ctx) { int status; pid_t zombie = waitpid (-1, &status, WNOHANG); if (zombie == -1) { // No children to wait on if (errno == ECHILD) return false; hard_assert (errno == EINTR); return true; } if (zombie == 0) return false; struct plugin *plugin = plugin_find_by_pid (ctx, zombie); // XXX: re-exec if something has died that we don't recognize? if (soft_assert (plugin != NULL)) on_plugin_death (plugin, status); return true; } static void kill_all_zombies (struct bot_context *ctx) { for (struct plugin *plugin = ctx->plugins; plugin; plugin = plugin->next) { if (!plugin->is_zombie) continue; print_status ("forcefully killing a zombie of `%s' (PID %d)", plugin->name, (int) plugin->pid); kill (plugin->pid, SIGKILL); } } static void on_signal_pipe_readable (const struct pollfd *fd, struct bot_context *ctx) { char dummy; (void) read (fd->fd, &dummy, 1); if (g_termination_requested) { g_termination_requested = false; if (!ctx->quitting) { // There may be a timer set to reconnect to the server irc_cancel_timers (ctx); if (ctx->irc_fd != -1) irc_send (ctx, "QUIT :Terminated by signal"); initiate_quit (ctx); } else // Disregard proper termination, just kill all the children kill_all_zombies (ctx); } // Reap all dead children (since the signal pipe may overflow etc. we run // waitpid() in a loop to return all the zombies it knows about). while (try_reap_plugin (ctx)) ; } int main (int argc, char *argv[]) { g_original_argv = strv_make (); strv_append_vector (&g_original_argv, argv); static const struct opt opts[] = { { 'd', "debug", NULL, 0, "run in debug mode" }, { 'h', "help", NULL, 0, "display this help and exit" }, { 'V', "version", NULL, 0, "output version information and exit" }, { 'w', "write-default-cfg", "FILENAME", OPT_OPTIONAL_ARG | OPT_LONG_ONLY, "write a default configuration file and exit" }, { 0, NULL, NULL, 0, NULL } }; struct opt_handler oh = opt_handler_make (argc, argv, opts, NULL, "Experimental IRC bot."); int c; while ((c = opt_handler_get (&oh)) != -1) switch (c) { case 'd': g_debug_mode = true; break; case 'h': opt_handler_usage (&oh, stdout); exit (EXIT_SUCCESS); case 'V': printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); exit (EXIT_SUCCESS); case 'w': call_simple_config_write_default (optarg, g_config_table); exit (EXIT_SUCCESS); default: print_error ("wrong options"); opt_handler_usage (&oh, stderr); exit (EXIT_FAILURE); } opt_handler_free (&oh); print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting"); setup_signal_handlers (); init_openssl (); struct bot_context ctx; bot_context_init (&ctx); struct error *e = NULL; if (!simple_config_update_from_file (&ctx.config, &e) || !setup_recovery_handler (&ctx, &e)) { print_error ("%s", e->message); error_free (e); exit (EXIT_FAILURE); } ctx.signal_event = poller_fd_make (&ctx.poller, g_signal_pipe[0]); ctx.signal_event.dispatcher = (poller_fd_fn) on_signal_pipe_readable; ctx.signal_event.user_data = &ctx; poller_fd_set (&ctx.signal_event, POLLIN); #if OpenBSD >= 201605 // cpath is for creating the plugin home directory if (pledge ("stdio rpath cpath inet proc exec", NULL)) exit_fatal ("%s: %s", "pledge", strerror (errno)); #endif plugin_load_all_from_config (&ctx); if (!parse_config (&ctx, &e) || !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); strv_free (&g_original_argv); return EXIT_SUCCESS; }