/* * zyklonb.c: the experimental IRC bot * * Copyright (c) 2014, Přemysl Janouch * 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" #include // --- 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" }, { "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", 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 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 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) { // 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_data *); 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_data *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_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) || !SSL_use_PrivateKey_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, 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! 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) { error_set (e, "connection failed"); return false; } ctx->irc_fd = sockfd; 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)); } // --- SOCKS 5/4a (blocking implementation) ------------------------------------ // These are awkward protocols. Note that the `username' is used differently // in SOCKS 4a and 5. In the former version, it is the username that you can // get ident'ed against. In the latter version, it forms a pair with the // password field and doesn't need to be an actual user on your machine. // TODO: make a non-blocking poller-based version of this; we need c-ares struct socks_addr { enum socks_addr_type { SOCKS_IPV4 = 1, ///< IPv4 address SOCKS_DOMAIN = 3, ///< Domain name to be resolved SOCKS_IPV6 = 4 ///< IPv6 address } type; ///< The type of this address union { uint8_t ipv4[4]; ///< IPv4 address, network octet order const char *domain; ///< Domain name uint8_t ipv6[16]; ///< IPv6 address, network octet order } data; ///< The address itself }; struct socks_data { struct socks_addr address; ///< Target address uint16_t port; ///< Target port const char *username; ///< Authentication username const char *password; ///< Authentication password struct socks_addr bound_address; ///< Bound address at the server uint16_t bound_port; ///< Bound port at the server }; static bool socks_get_socket (struct addrinfo *addresses, int *fd, struct error **e) { int sockfd; for (; addresses; addresses = addresses->ai_next) { sockfd = socket (addresses->ai_family, addresses->ai_socktype, addresses->ai_protocol); if (sockfd == -1) continue; set_cloexec (sockfd); int yes = 1; soft_assert (setsockopt (sockfd, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof yes) != -1); if (!connect (sockfd, addresses->ai_addr, addresses->ai_addrlen)) break; xclose (sockfd); } if (!addresses) { error_set (e, "couldn't connect to the SOCKS server"); return false; } *fd = sockfd; return true; } #define SOCKS_FAIL(...) \ BLOCK_START \ error_set (e, __VA_ARGS__); \ goto fail; \ BLOCK_END #define SOCKS_RECV(buf, len) \ BLOCK_START \ if ((n = recv (sockfd, (buf), (len), 0)) == -1) \ SOCKS_FAIL ("%s: %s", "recv", strerror (errno)); \ if (n != (len)) \ SOCKS_FAIL ("%s: %s", "protocol error", "unexpected EOF"); \ BLOCK_END static bool socks_4a_connect (struct addrinfo *addresses, struct socks_data *data, int *fd, struct error **e) { int sockfd; if (!socks_get_socket (addresses, &sockfd, e)) return false; const void *dest_ipv4 = "\x00\x00\x00\x01"; const char *dest_domain = NULL; char buf[INET6_ADDRSTRLEN]; switch (data->address.type) { case SOCKS_IPV4: dest_ipv4 = data->address.data.ipv4; break; case SOCKS_IPV6: // About the best thing we can do, not sure if it works anywhere at all if (!inet_ntop (AF_INET6, &data->address.data.ipv6, buf, sizeof buf)) SOCKS_FAIL ("%s: %s", "inet_ntop", strerror (errno)); dest_domain = buf; break; case SOCKS_DOMAIN: dest_domain = data->address.data.domain; } struct str req; str_init (&req); str_append_c (&req, 4); // version str_append_c (&req, 1); // connect str_append_c (&req, data->port >> 8); // higher bits of port str_append_c (&req, data->port); // lower bits of port str_append_data (&req, dest_ipv4, 4); // destination address if (data->username) str_append (&req, data->username); str_append_c (&req, '\0'); if (dest_domain) { str_append (&req, dest_domain); str_append_c (&req, '\0'); } ssize_t n = send (sockfd, req.str, req.len, 0); str_free (&req); if (n == -1) SOCKS_FAIL ("%s: %s", "send", strerror (errno)); uint8_t resp[8]; SOCKS_RECV (resp, sizeof resp); if (resp[0] != 0) SOCKS_FAIL ("protocol error"); switch (resp[1]) { case 90: break; case 91: SOCKS_FAIL ("request rejected or failed"); case 92: SOCKS_FAIL ("%s: %s", "request rejected", "SOCKS server cannot connect to identd on the client"); case 93: SOCKS_FAIL ("%s: %s", "request rejected", "identd reports different user-id"); default: SOCKS_FAIL ("protocol error"); } *fd = sockfd; return true; fail: xclose (sockfd); return false; } #undef SOCKS_FAIL #define SOCKS_FAIL(...) \ BLOCK_START \ error_set (e, __VA_ARGS__); \ return false; \ BLOCK_END static bool socks_5_userpass_auth (int sockfd, struct socks_data *data, struct error **e) { size_t ulen = strlen (data->username); if (ulen > 255) ulen = 255; size_t plen = strlen (data->password); if (plen > 255) plen = 255; uint8_t req[3 + ulen + plen], *p = req; *p++ = 0x01; // version *p++ = ulen; // username length memcpy (p, data->username, ulen); p += ulen; *p++ = plen; // password length memcpy (p, data->password, plen); p += plen; ssize_t n = send (sockfd, req, p - req, 0); if (n == -1) SOCKS_FAIL ("%s: %s", "send", strerror (errno)); uint8_t resp[2]; SOCKS_RECV (resp, sizeof resp); if (resp[0] != 0x01) SOCKS_FAIL ("protocol error"); if (resp[1] != 0x00) SOCKS_FAIL ("authentication failure"); return true; } static bool socks_5_auth (int sockfd, struct socks_data *data, struct error **e) { bool can_auth = data->username && data->password; uint8_t hello[4]; hello[0] = 0x05; // version hello[1] = 1 + can_auth; // number of authentication methods hello[2] = 0x00; // no authentication required hello[3] = 0x02; // username/password ssize_t n = send (sockfd, hello, 3 + can_auth, 0); if (n == -1) SOCKS_FAIL ("%s: %s", "send", strerror (errno)); uint8_t resp[2]; SOCKS_RECV (resp, sizeof resp); if (resp[0] != 0x05) SOCKS_FAIL ("protocol error"); switch (resp[1]) { case 0x02: if (!can_auth) SOCKS_FAIL ("protocol error"); if (!socks_5_userpass_auth (sockfd, data, e)) return false; case 0x00: break; case 0xFF: SOCKS_FAIL ("no acceptable authentication methods"); default: SOCKS_FAIL ("protocol error"); } return true; } static bool socks_5_send_req (int sockfd, struct socks_data *data, struct error **e) { uint8_t req[4 + 256 + 2], *p = req; *p++ = 0x05; // version *p++ = 0x01; // connect *p++ = 0x00; // reserved *p++ = data->address.type; switch (data->address.type) { case SOCKS_IPV4: memcpy (p, data->address.data.ipv4, sizeof data->address.data.ipv4); p += sizeof data->address.data.ipv4; break; case SOCKS_DOMAIN: { size_t dlen = strlen (data->address.data.domain); if (dlen > 255) dlen = 255; *p++ = dlen; memcpy (p, data->address.data.domain, dlen); p += dlen; break; } case SOCKS_IPV6: memcpy (p, data->address.data.ipv6, sizeof data->address.data.ipv6); p += sizeof data->address.data.ipv6; break; } *p++ = data->port >> 8; *p++ = data->port; if (send (sockfd, req, p - req, 0) == -1) SOCKS_FAIL ("%s: %s", "send", strerror (errno)); return true; } static bool socks_5_process_resp (int sockfd, struct socks_data *data, struct error **e) { uint8_t resp_header[4]; ssize_t n; SOCKS_RECV (resp_header, sizeof resp_header); if (resp_header[0] != 0x05) SOCKS_FAIL ("protocol error"); switch (resp_header[1]) { case 0x00: break; case 0x01: SOCKS_FAIL ("general SOCKS server failure"); case 0x02: SOCKS_FAIL ("connection not allowed by ruleset"); case 0x03: SOCKS_FAIL ("network unreachable"); case 0x04: SOCKS_FAIL ("host unreachable"); case 0x05: SOCKS_FAIL ("connection refused"); case 0x06: SOCKS_FAIL ("TTL expired"); case 0x07: SOCKS_FAIL ("command not supported"); case 0x08: SOCKS_FAIL ("address type not supported"); default: SOCKS_FAIL ("protocol error"); } switch ((data->bound_address.type = resp_header[3])) { case SOCKS_IPV4: SOCKS_RECV (data->bound_address.data.ipv4, sizeof data->bound_address.data.ipv4); break; case SOCKS_IPV6: SOCKS_RECV (data->bound_address.data.ipv6, sizeof data->bound_address.data.ipv6); break; case SOCKS_DOMAIN: { uint8_t len; SOCKS_RECV (&len, sizeof len); char domain[len + 1]; SOCKS_RECV (domain, len); domain[len] = '\0'; data->bound_address.data.domain = xstrdup (domain); } default: SOCKS_FAIL ("protocol error"); } uint16_t port; SOCKS_RECV (&port, sizeof port); data->bound_port = ntohs (port); return true; } #undef SOCKS_FAIL #undef SOCKS_RECV static bool socks_5_connect (struct addrinfo *addresses, struct socks_data *data, int *fd, struct error **e) { int sockfd; if (!socks_get_socket (addresses, &sockfd, e)) return false; if (!socks_5_auth (sockfd, data, e) || !socks_5_send_req (sockfd, data, e) || !socks_5_process_resp (sockfd, data, e)) { xclose (sockfd); return false; } *fd = sockfd; return true; } static int socks_connect (const char *socks_host, const char *socks_port, const char *host, const char *port, const char *username, const char *password, struct error **e) { struct addrinfo gai_hints, *gai_result; memset (&gai_hints, 0, sizeof gai_hints); gai_hints.ai_socktype = SOCK_STREAM; int err = getaddrinfo (socks_host, socks_port, &gai_hints, &gai_result); if (err) { error_set (e, "%s: %s", "getaddrinfo", gai_strerror (err)); return false; } unsigned long port_no; const struct servent *serv; if ((serv = getservbyname (port, "tcp"))) port_no = (uint16_t) ntohs (serv->s_port); else if (!xstrtoul (&port_no, port, 10) || !port_no || port_no > UINT16_MAX) { error_set (e, "invalid port number"); return false; } struct socks_data data = { .username = username, .password = password, .port = port_no }; if (inet_pton (AF_INET, host, &data.address.data.ipv4) == 1) data.address.type = SOCKS_IPV4; else if (inet_pton (AF_INET6, host, &data.address.data.ipv6) == 1) data.address.type = SOCKS_IPV6; else { data.address.type = SOCKS_DOMAIN; data.address.data.domain = host; } int fd; bool success = socks_5_connect (gai_result, &data, &fd, NULL) || socks_4a_connect (gai_result, &data, &fd, e); freeaddrinfo (gai_result); if (data.bound_address.type == SOCKS_DOMAIN) free ((char *) data.bound_address.data.domain); return success ? fd : -1; } // --- 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; // 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_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. 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) { // 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 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; \x02plugins:\x0f ", reason); size_t zombies = 0; const char *prepend = ""; for (struct plugin_data *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 (&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 *, struct error **); static void irc_queue_reconnect (struct bot_context *); static void irc_cancel_timers (struct bot_context *ctx) { ssize_t i; struct poller_timers *timers = &ctx->poller.timers; while ((i = poller_timers_find_by_data (timers, ctx)) != -1) poller_timers_remove_at_index (timers, i); } static void irc_on_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_timers_add (&ctx->poller.timers, irc_on_reconnect_timeout, ctx, 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; } 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 // 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_timers_add (&ctx->poller.timers, on_irc_timeout, ctx, 3 * 60 * 1000); poller_timers_add (&ctx->poller.timers, on_irc_ping_timeout, ctx, (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_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); else irc_reset_connection_timeouts (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 *socks_host = str_map_find (&ctx->config, "socks_host"); const char *socks_port = str_map_find (&ctx->config, "socks_port"); const char *socks_username = str_map_find (&ctx->config, "socks_username"); const char *socks_password = str_map_find (&ctx->config, "socks_password"); 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 && 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) { 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 (socks_host) { char *address = format_host_port_pair (irc_host, irc_port); char *socks_address = format_host_port_pair (socks_host, socks_port); print_status ("connecting to %s via %s...", address, socks_address); free (socks_address); free (address); struct error *error = NULL; int fd = socks_connect (socks_host, socks_port, irc_host, irc_port, socks_username, socks_password, &error); if (fd == -1) { error_set (e, "%s: %s", "SOCKS connection failed", error->message); error_free (error); return false; } ctx->irc_fd = fd; } else if (!irc_establish_connection (ctx, irc_host, irc_port, e)) return false; if (use_ssl && !irc_initialize_ssl (ctx, e)) { xclose (ctx->irc_fd); ctx->irc_fd = -1; return false; } print_status ("connection established"); // 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); 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) { const char *reconnect_str = str_map_find (&ctx->config, "reconnect"); hard_assert (reconnect_str != NULL); // We have a default value for this if (!set_boolean_if_valid (&ctx->reconnect, reconnect_str)) { error_set (e, "invalid configuration value for `%s'", "reconnect"); 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)) { error_set (e, "invalid configuration value for `%s'", "reconnect_delay"); return false; } 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_signal_pipe_readable (const struct pollfd *fd, struct bot_context *ctx) { char *dummy; (void) read (fd->fd, &dummy, 1); if (g_termination_requested && !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); } // 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 (!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); str_vector_free (&g_original_argv); return EXIT_SUCCESS; }