/* * degesch.c: the experimental IRC client * * Copyright (c) 2015, 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. * */ /// Some arbitrary limit for the history file #define HISTORY_LIMIT 10000 // String constants for all attributes we use for output #define ATTR_PROMPT "attr_prompt" #define ATTR_RESET "attr_reset" #define ATTR_WARNING "attr_warning" #define ATTR_ERROR "attr_error" // User data for logger functions to enable formatted logging #define print_fatal_data ATTR_ERROR #define print_error_data ATTR_ERROR #define print_warning_data ATTR_WARNING #include "config.h" #undef PROGRAM_NAME #define PROGRAM_NAME "degesch" #include "common.c" #include #include #include #include #include #include #include // --- Configuration (application-specific) ------------------------------------ static struct config_item g_config_table[] = { { ATTR_PROMPT, NULL, "Terminal attributes for the prompt" }, { ATTR_RESET, NULL, "String to reset terminal attributes" }, { ATTR_WARNING, NULL, "Terminal attributes for warnings" }, { ATTR_ERROR, NULL, "Terminal attributes for errors" }, { "nickname", NULL, "IRC nickname" }, { "username", NULL, "IRC user name" }, { "realname", NULL, "IRC real name/e-mail" }, { "irc_host", NULL, "Address of the IRC server" }, { "irc_port", "6667", "Port of the IRC server" }, { "ssl", "off", "Whether to use SSL" }, { "ssl_cert", NULL, "Client SSL certificate (PEM)" }, { "ssl_verify", "on", "Whether to verify certificates" }, { "ssl_ca_file", NULL, "OpenSSL CA bundle file" }, { "ssl_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" }, { NULL, NULL, NULL } }; // --- Application data -------------------------------------------------------- /// Shorthand to set an error and return failure from the function #define FAIL(...) \ BLOCK_START \ error_set (e, __VA_ARGS__); \ return false; \ BLOCK_END // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - enum buffer_line_flags { BUFFER_LINE_HIGHLIGHT = 1 << 0 ///< The user was highlighted by this }; enum buffer_line_type { BUFFER_LINE_TEXT, ///< PRIVMSG BUFFER_LINE_NOTICE, ///< NOTICE BUFFER_LINE_STATUS ///< JOIN, PART, QUIT }; struct buffer_line { LIST_HEADER (struct buffer_line) enum buffer_line_type type; ///< Type of the event int flags; ///< Flags time_t when; ///< Time of the event char *origin; ///< Name of the origin char *text; ///< The text of the message }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct nick_info { char *nickname; ///< Literal nickname char mode_char; ///< Op/voice/... character bool away; ///< User is away // XXX: maybe a good candidate for deduplication (away status) }; static void nick_info_destroy (void *p) { struct nick_info *self = p; free (self->nickname); free (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - enum buffer_type { BUFFER_GLOBAL, ///< Global information BUFFER_SERVER, ///< Server-related messages BUFFER_CHANNEL, ///< Channels BUFFER_PM ///< Private messages (query) }; struct buffer { LIST_HEADER (struct buffer) enum buffer_type type; ///< Type of the buffer char *name; ///< The name of the buffer unsigned unseen_messages; ///< # messages since last visited // TODO: now I can't just print messages with print_status() etc., // all that stuff has to go into a buffer now // Channels: char *topic; ///< Channel topic struct str_map nicks; ///< Maps nicks to "nick_info" }; static struct buffer * buffer_new (void) { struct buffer *self = xcalloc (1, sizeof *self); str_map_init (&self->nicks); self->nicks.key_xfrm = irc_strxfrm; self->nicks.free = nick_info_destroy; return self; } static void buffer_destroy (struct buffer *self) { free (self->name); free (self->topic); str_map_free (&self->nicks); free (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - enum color_mode { COLOR_AUTO, ///< Autodetect if colours are available COLOR_ALWAYS, ///< Always use coloured output COLOR_NEVER ///< Never use coloured output }; struct app_context { struct str_map config; ///< User configuration enum color_mode color_mode; ///< Colour output mode 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_ready; ///< Whether we may send messages now struct poller_fd tty_event; ///< Terminal input event 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 buffer *buffers; ///< All our buffers in order struct buffer *buffers_tail; ///< The tail of our buffers // TODO: a name -> buffer map that excludes GLOBAL and SERVER struct buffer *global_buffer; ///< The global buffer struct buffer *server_buffer; ///< The server buffer struct buffer *current_buffer; ///< The current buffer // TODO: So that we always output proper date change messages time_t last_displayed_msg_time; ///< Time of last displayed message struct poller poller; ///< Manages polled descriptors bool quitting; ///< User requested quitting bool polling; ///< The event loop is running iconv_t term_to_utf8; ///< Terminal encoding to UTF-8 iconv_t term_from_utf8; ///< UTF-8 to terminal encoding iconv_t term_from_latin1; ///< ISO Latin 1 to terminal encoding char *readline_prompt; ///< The prompt we use for readline bool readline_prompt_shown; ///< Whether the prompt is shown now } *g_ctx; 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 app_context_init (struct app_context *self) { memset (self, 0, sizeof *self); str_map_init (&self->config); self->config.free = free; load_config_defaults (&self->config, g_config_table); self->irc_fd = -1; str_init (&self->read_buffer); self->irc_ready = false; self->last_displayed_msg_time = time (NULL); poller_init (&self->poller); char *encoding = nl_langinfo (CODESET); #ifdef __linux__ encoding = xstrdup_printf ("%s//TRANSLIT", encoding); #else // ! __linux__ encoding = xstrdup (encoding); #endif // ! __linux__ if ((self->term_from_utf8 = iconv_open (encoding, "UTF-8")) == (iconv_t) -1 || (self->term_from_latin1 = iconv_open (encoding, "ISO-8859-1")) == (iconv_t) -1 || (self->term_to_utf8 = iconv_open ("UTF-8", nl_langinfo (CODESET))) == (iconv_t) -1) exit_fatal ("creating the UTF-8 conversion object failed: %s", strerror (errno)); free (encoding); } static void app_context_free (struct app_context *self) { str_map_free (&self->config); str_free (&self->read_buffer); if (self->irc_fd != -1) { xclose (self->irc_fd); poller_fd_reset (&self->irc_event); } if (self->ssl) SSL_free (self->ssl); if (self->ssl_ctx) SSL_CTX_free (self->ssl_ctx); poller_free (&self->poller); free (self->readline_prompt); iconv_close (self->term_from_latin1); iconv_close (self->term_from_utf8); iconv_close (self->term_to_utf8); } // --- Attributed output ------------------------------------------------------- static struct { bool initialized; ///< Terminal is available bool stdout_is_tty; ///< `stdout' is a terminal bool stderr_is_tty; ///< `stderr' is a terminal char *color_set[8]; ///< Codes to set the foreground colour } g_terminal; static bool init_terminal (void) { int tty_fd = -1; if ((g_terminal.stderr_is_tty = isatty (STDERR_FILENO))) tty_fd = STDERR_FILENO; if ((g_terminal.stdout_is_tty = isatty (STDOUT_FILENO))) tty_fd = STDOUT_FILENO; int err; if (tty_fd == -1 || setupterm (NULL, tty_fd, &err) == ERR) return false; // Make sure all terminal features used by us are supported if (!set_a_foreground || !enter_bold_mode || !exit_attribute_mode) { del_curterm (cur_term); return false; } for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set); i++) g_terminal.color_set[i] = xstrdup (tparm (set_a_foreground, i, 0, 0, 0, 0, 0, 0, 0, 0)); return g_terminal.initialized = true; } static void free_terminal (void) { if (!g_terminal.initialized) return; for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set); i++) free (g_terminal.color_set[i]); del_curterm (cur_term); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct app_readline_state { char *saved_line; int saved_point; }; static void app_readline_hide (struct app_readline_state *state) { state->saved_point = rl_point; state->saved_line = rl_copy_text (0, rl_end); rl_set_prompt (""); rl_replace_line ("", 0); rl_redisplay (); } static void app_readline_restore (struct app_readline_state *state, const char *prompt) { rl_set_prompt (prompt); rl_replace_line (state->saved_line, 0); rl_point = state->saved_point; rl_redisplay (); free (state->saved_line); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - typedef int (*terminal_printer_fn) (int); static int putchar_stderr (int c) { return fputc (c, stderr); } static terminal_printer_fn get_attribute_printer (FILE *stream) { if (stream == stdout && g_terminal.stdout_is_tty) return putchar; if (stream == stderr && g_terminal.stderr_is_tty) return putchar_stderr; return NULL; } static void vprint_attributed (struct app_context *ctx, FILE *stream, const char *attribute, const char *fmt, va_list ap) { terminal_printer_fn printer = get_attribute_printer (stream); if (!attribute) printer = NULL; if (printer) { const char *value = str_map_find (&ctx->config, attribute); tputs (value, 1, printer); } vfprintf (stream, fmt, ap); if (printer) { const char *value = str_map_find (&ctx->config, ATTR_RESET); tputs (value, 1, printer); } } static void print_attributed (struct app_context *ctx, FILE *stream, const char *attribute, const char *fmt, ...) { va_list ap; va_start (ap, fmt); vprint_attributed (ctx, stream, attribute, fmt, ap); va_end (ap); } static void log_message_attributed (void *user_data, const char *quote, const char *fmt, va_list ap) { FILE *stream = stderr; // GNU readline is a huge piece of total crap; it seems that we must do // these incredible shenanigans in order to intersperse readline output // with asynchronous status messages struct app_readline_state state; if (g_ctx->readline_prompt_shown) app_readline_hide (&state); print_attributed (g_ctx, stream, user_data, "%s", quote); vprint_attributed (g_ctx, stream, user_data, fmt, ap); fputs ("\n", stream); if (g_ctx->readline_prompt_shown) app_readline_restore (&state, g_ctx->readline_prompt); } static void init_colors (struct app_context *ctx) { // Use escape sequences from terminfo if possible, and SGR as a fallback if (init_terminal ()) { const char *attrs[][2] = { { ATTR_PROMPT, enter_bold_mode }, { ATTR_RESET, exit_attribute_mode }, { ATTR_WARNING, g_terminal.color_set[3] }, { ATTR_ERROR, g_terminal.color_set[1] }, }; for (size_t i = 0; i < N_ELEMENTS (attrs); i++) str_map_set (&ctx->config, attrs[i][0], xstrdup (attrs[i][1])); } else { const char *attrs[][2] = { { ATTR_PROMPT, "\x1b[1m" }, { ATTR_RESET, "\x1b[0m" }, { ATTR_WARNING, "\x1b[33m" }, { ATTR_ERROR, "\x1b[31m" }, }; for (size_t i = 0; i < N_ELEMENTS (attrs); i++) str_map_set (&ctx->config, attrs[i][0], xstrdup (attrs[i][1])); } switch (ctx->color_mode) { case COLOR_ALWAYS: g_terminal.stdout_is_tty = true; g_terminal.stderr_is_tty = true; break; case COLOR_AUTO: if (!g_terminal.initialized) { case COLOR_NEVER: g_terminal.stdout_is_tty = false; g_terminal.stderr_is_tty = false; } } g_log_message_real = log_message_attributed; } // --- Signals ----------------------------------------------------------------- static int g_signal_pipe[2]; ///< A pipe used to signal... signals /// Program termination has been requested by a signal static volatile sig_atomic_t g_termination_requested; /// The window has changed in size static volatile sig_atomic_t g_winch_received; 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 sigwinch_handler (int signum) { (void) signum; g_winch_received = true; int original_errno = errno; if (write (g_signal_pipe[1], "w", 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); signal (SIGPIPE, SIG_IGN); struct sigaction sa; sa.sa_flags = SA_RESTART; sa.sa_handler = sigwinch_handler; sigemptyset (&sa.sa_mask); if (sigaction (SIGWINCH, &sa, NULL) == -1) exit_fatal ("sigaction: %s", strerror (errno)); sa.sa_handler = sigterm_handler; if (sigaction (SIGINT, &sa, NULL) == -1 || sigaction (SIGTERM, &sa, NULL) == -1) exit_fatal ("sigaction: %s", strerror (errno)); } // --- Buffers ----------------------------------------------------------------- static void send_to_buffer (struct app_context *ctx, struct buffer *buffer, enum buffer_line_type type, int flags, const char *origin, const char *format, ...); static void prepare_buffers (struct app_context *ctx) { struct buffer *global = ctx->global_buffer = buffer_new (); struct buffer *server = ctx->server_buffer = buffer_new (); global->type = BUFFER_GLOBAL; global->name = xstrdup (PROGRAM_NAME); server->type = BUFFER_SERVER; server->name = xstrdup (str_map_find (&ctx->config, "irc_host")); LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, global); LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, server); } // --- Supporting code --------------------------------------------------------- static void irc_shutdown (struct app_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 app_context *ctx) { if (ctx->quitting && ctx->irc_fd == -1) ctx->polling = false; } static void initiate_quit (struct app_context *ctx) { print_status ("shutting down"); if (ctx->irc_fd != -1) irc_shutdown (ctx); ctx->quitting = true; try_finish_quit (ctx); } static bool irc_send (struct app_context *ctx, const char *format, ...) ATTRIBUTE_PRINTF (2, 3); static bool irc_send (struct app_context *ctx, const char *format, ...) { va_list ap; if (g_debug_mode) { struct app_readline_state state; if (ctx->readline_prompt_shown) app_readline_hide (&state); fputs ("[IRC] <== \"", stderr); va_start (ap, format); vfprintf (stderr, format, ap); va_end (ap); fputs ("\"\n", stderr); if (ctx->readline_prompt_shown) app_readline_restore (&state, ctx->readline_prompt); } 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_get_boolean_from_config (struct app_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; error_set (e, "invalid configuration value for `%s'", name); return false; } static bool irc_initialize_ssl_ctx (struct app_context *ctx, struct error **e) { // XXX: maybe we should call SSL_CTX_set_options() for some workarounds bool verify; if (!irc_get_boolean_from_config (ctx, "ssl_verify", &verify, e)) return false; if (!verify) SSL_CTX_set_verify (ctx->ssl_ctx, SSL_VERIFY_NONE, NULL); const char *ca_file = str_map_find (&ctx->config, "ca_file"); const char *ca_path = str_map_find (&ctx->config, "ca_path"); struct error *error = NULL; if (ca_file || ca_path) { if (SSL_CTX_load_verify_locations (ctx->ssl_ctx, ca_file, ca_path)) return true; error_set (&error, "%s: %s", "failed to set locations for the CA certificate bundle", ERR_reason_error_string (ERR_get_error ())); goto ca_error; } if (!SSL_CTX_set_default_verify_paths (ctx->ssl_ctx)) { error_set (&error, "%s: %s", "couldn't load the default CA certificate bundle", ERR_reason_error_string (ERR_get_error ())); goto ca_error; } return true; ca_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_ssl (struct app_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 *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); 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); error_set (e, "%s: %s", "could not initialize SSL", error_info); return false; } static bool irc_establish_connection (struct app_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) { 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, NI_NUMERICHOST); 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; } static void refresh_prompt (struct app_context *ctx) { bool have_attributes = !!get_attribute_printer (stdout); struct str prompt; str_init (&prompt); if (!ctx->irc_ready) str_append (&prompt, "(disconnected)"); else { str_append_c (&prompt, '['); // TODO: go through all buffers and prepend the active ones, // such as "(1,4) " // TODO: name of the current buffer // TODO: the channel user mode str_append (&prompt, str_map_find (&ctx->config, "nickname")); // TODO: user mode in parenthesis str_append_c (&prompt, ']'); } str_append (&prompt, " "); // After building the new prompt, replace the old one free (ctx->readline_prompt); if (!have_attributes) ctx->readline_prompt = xstrdup (prompt.str); else { // XXX: to be completely correct, we should use tputs, but we cannot const char *prompt_attrs = str_map_find (&ctx->config, ATTR_PROMPT); const char *reset_attrs = str_map_find (&ctx->config, ATTR_RESET); ctx->readline_prompt = xstrdup_printf ("%c%s%c%s%c%s%c", RL_PROMPT_START_IGNORE, prompt_attrs, RL_PROMPT_END_IGNORE, prompt.str, RL_PROMPT_START_IGNORE, reset_attrs, RL_PROMPT_END_IGNORE); } str_free (&prompt); // FIXME: when the program hasn't displayed the prompt yet, is this okay? rl_redisplay (); } // --- Input handling ---------------------------------------------------------- static void irc_process_message (const struct irc_message *msg, const char *raw, void *user_data) { struct app_context *ctx = user_data; if (g_debug_mode) { struct app_readline_state state; if (ctx->readline_prompt_shown) app_readline_hide (&state); // TODO: ensure proper encoding fprintf (stderr, "[%s] ==> \"%s\"\n", "IRC", raw); if (ctx->readline_prompt_shown) app_readline_restore (&state, ctx->readline_prompt); } bool show_to_user = true; if (!strcasecmp (msg->command, "PING")) { show_to_user = false; 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 { // TODO: whatever processing we need } // This is going to be a lot more complicated if (show_to_user) // TODO: ensure proper encoding print_status ("%s", raw); } // TODO: load and preprocess this table so that shortcuts are accepted struct command_handler { char *name; void (*handler) (struct app_context *ctx, const char *arguments); } g_handlers[] = { { "buffer", NULL }, { "help", NULL }, { "msg", NULL }, { "query", NULL }, { "notice", NULL }, { "ctcp", NULL }, { "me", NULL }, { "join", NULL }, { "part", NULL }, { "cycle", NULL }, { "mode", NULL }, { "topic", NULL }, { "kick", NULL }, { "kickban", NULL }, { "ban", NULL }, { "invite", NULL }, { "list", NULL }, { "names", NULL }, { "who", NULL }, { "whois", NULL }, { "motd", NULL }, { "away", NULL }, { "quote", NULL }, { "quit", NULL }, }; static void process_internal_command (struct app_context *ctx, const char *command) { // TODO: resolve commands from a map // TODO: if it's a number, switch to the given buffer if (!strcmp (command, "quit")) initiate_quit (ctx); } static void process_input (struct app_context *ctx, char *user_input) { char *input; size_t len; if (!(input = iconv_xstrdup (ctx->term_to_utf8, user_input, -1, &len))) { print_error ("character conversion failed for `%s'", "user input"); goto fail; } if (*input == '/') process_internal_command (ctx, input + 1); else { // TODO: send a message to the current buffer } fail: free (input); } static int on_readline_previous_buffer (int count, int key) { (void) key; // TODO: switch to the previous buffer return 0; } static int on_readline_next_buffer (int count, int key) { (void) key; // TODO: switch to the next buffer return 0; } // --- Supporting code (continued) --------------------------------------------- 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 app_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 app_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 app_context *, struct error **); static void irc_queue_reconnect (struct app_context *); static void irc_cancel_timers (struct app_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 app_context *ctx = user_data; struct error *e = NULL; if (irc_connect (ctx, &e)) return; print_error ("%s", e->message); error_free (e); irc_queue_reconnect (ctx); } static void irc_queue_reconnect (struct app_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 app_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; } xclose (ctx->irc_fd); ctx->irc_fd = -1; ctx->irc_ready = false; ctx->irc_event.closed = true; poller_fd_reset (&ctx->irc_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 app_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 app_context *ctx = user_data; irc_send (ctx, "PING :%s", (char *) str_map_find (&ctx->config, "nickname")); } static void irc_reset_connection_timeouts (struct app_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 app_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 app_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 app_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 *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 && socks_port); // These are filled automatically if needed 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 (!irc_get_boolean_from_config (ctx, "ssl", &use_ssl, e)) 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"); poller_fd_init (&ctx->irc_event, &ctx->poller, ctx->irc_fd); ctx->irc_event.dispatcher = (poller_fd_fn) on_irc_readable; ctx->irc_event.user_data = ctx; 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; } // --- I/O event handlers ------------------------------------------------------ static void on_signal_pipe_readable (const struct pollfd *fd, struct app_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); } if (g_winch_received) // This fucks up big time on terminals with automatic wrapping such as // rxvt-unicode or newer VTE when the current line overflows, however we // can't do much about that rl_resize_terminal (); } static void on_tty_readable (const struct pollfd *fd, struct app_context *ctx) { (void) ctx; if (fd->revents & ~(POLLIN | POLLHUP | POLLERR)) print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents); rl_callback_read_char (); } static void on_readline_input (char *line) { // Otherwise the prompt is shown at all times g_ctx->readline_prompt_shown = false; if (line) { if (*line) add_history (line); process_input (g_ctx, line); free (line); } g_ctx->readline_prompt_shown = true; } // --- Configuration loading --------------------------------------------------- static bool read_hexa_escape (const char **cursor, struct str *output) { int i; char c, code = 0; for (i = 0; i < 2; i++) { c = tolower (*(*cursor)); if (c >= '0' && c <= '9') code = (code << 4) | (c - '0'); else if (c >= 'a' && c <= 'f') code = (code << 4) | (c - 'a' + 10); else break; (*cursor)++; } if (!i) return false; str_append_c (output, code); return true; } static bool read_octal_escape (const char **cursor, struct str *output) { int i; char c, code = 0; for (i = 0; i < 3; i++) { c = *(*cursor); if (c < '0' || c > '7') break; code = (code << 3) | (c - '0'); (*cursor)++; } if (!i) return false; str_append_c (output, code); return true; } static bool read_string_escape_sequence (const char **cursor, struct str *output, struct error **e) { int c; switch ((c = *(*cursor)++)) { case '?': str_append_c (output, '?'); break; case '"': str_append_c (output, '"'); break; case '\\': str_append_c (output, '\\'); break; case 'a': str_append_c (output, '\a'); break; case 'b': str_append_c (output, '\b'); break; case 'f': str_append_c (output, '\f'); break; case 'n': str_append_c (output, '\n'); break; case 'r': str_append_c (output, '\r'); break; case 't': str_append_c (output, '\t'); break; case 'v': str_append_c (output, '\v'); break; case 'e': case 'E': str_append_c (output, '\x1b'); break; case 'x': case 'X': if (!read_hexa_escape (cursor, output)) FAIL ("invalid hexadecimal escape"); break; case '\0': FAIL ("premature end of escape sequence"); default: (*cursor)--; if (!read_octal_escape (cursor, output)) FAIL ("unknown escape sequence"); } return true; } static bool unescape_string (const char *s, struct str *output, struct error **e) { int c; while ((c = *s++)) { if (c != '\\') str_append_c (output, c); else if (!read_string_escape_sequence (&s, output, e)) return false; } return true; } static bool autofill_user_info (struct app_context *ctx, struct error **e) { 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"); if (nickname && username && realname) return true; // TODO: read POSIX user info and fill the configuration if needed struct passwd *pwd = getpwuid (geteuid ()); if (!pwd) FAIL ("cannot retrieve user information: %s", strerror (errno)); if (!nickname) str_map_set (&ctx->config, "nickname", xstrdup (pwd->pw_name)); if (!username) str_map_set (&ctx->config, "username", xstrdup (pwd->pw_name)); // Not all systems have the GECOS field but the vast majority does if (!realname) { char *gecos = pwd->pw_gecos; // The first comma, if any, ends the user's real name char *comma = strchr (gecos, ','); if (comma) *comma = '\0'; str_map_set (&ctx->config, "username", xstrdup (gecos)); } return true; } static bool load_config (struct app_context *ctx, struct error **e) { // TODO: employ a better configuration file format, so that we don't have // to do this convoluted post-processing anymore. struct str_map map; str_map_init (&map); map.free = free; if (!read_config_file (&map, e)) return false; struct str_map_iter iter; str_map_iter_init (&iter, &map); while (str_map_iter_next (&iter)) { struct error *e = NULL; struct str value; str_init (&value); if (!unescape_string (iter.link->data, &value, &e)) { print_error ("error reading configuration: %s: %s", iter.link->key, e->message); error_free (e); exit (EXIT_FAILURE); } str_map_set (&ctx->config, iter.link->key, str_steal (&value)); } if (!autofill_user_info (ctx, e)) return false; 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)) { error_set (e, "invalid configuration value for `%s'", "reconnect_delay"); return false; } return true; } // --- Main program ------------------------------------------------------------ static void init_poller_events (struct app_context *ctx) { poller_timer_init (&ctx->timeout_tmr, &ctx->poller); ctx->timeout_tmr.dispatcher = on_irc_timeout; ctx->timeout_tmr.user_data = ctx; poller_timer_init (&ctx->ping_tmr, &ctx->poller); ctx->ping_tmr.dispatcher = on_irc_ping_timeout; ctx->ping_tmr.user_data = ctx; poller_timer_init (&ctx->reconnect_tmr, &ctx->poller); ctx->reconnect_tmr.dispatcher = on_irc_reconnect_timeout; ctx->reconnect_tmr.user_data = ctx; poller_fd_init (&ctx->signal_event, &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); poller_fd_init (&ctx->tty_event, &ctx->poller, STDIN_FILENO); ctx->tty_event.dispatcher = (poller_fd_fn) on_tty_readable; ctx->tty_event.user_data = &ctx; poller_fd_set (&ctx->tty_event, POLLIN); } int main (int argc, char *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_init (&oh, argc, argv, opts, NULL, "Experimental IRC client."); 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_write_default_config (optarg, g_config_table); exit (EXIT_SUCCESS); default: print_error ("wrong options"); opt_handler_usage (&oh, stderr); exit (EXIT_FAILURE); } opt_handler_free (&oh); struct app_context ctx; print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting"); app_context_init (&ctx); g_ctx = &ctx; // We only need to convert to and from the terminal encoding setlocale (LC_CTYPE, ""); SSL_library_init (); atexit (EVP_cleanup); SSL_load_error_strings (); atexit (ERR_free_strings); using_history (); stifle_history (HISTORY_LIMIT); init_colors (&ctx); init_poller_events (&ctx); struct error *e = NULL; if (!load_config (&ctx, &e) || !irc_connect (&ctx, &e)) { print_error ("%s", e->message); error_free (e); exit (EXIT_FAILURE); } setup_signal_handlers (); prepare_buffers (&ctx); refresh_prompt (&ctx); // TODO: maybe use rl_make_bare_keymap() and start from there // XXX: Since readline() installs a set of default key bindings the first // time it is called, there is always the danger that a custom binding // installed before the first call to readline() will be overridden. // An alternate mechanism is to install custom key bindings in an // initialization function assigned to the rl_startup_hook variable. rl_add_defun ("previous-buffer", on_readline_previous_buffer, -1); rl_add_defun ("next-buffer", on_readline_next_buffer, -1); // TODO: redefine M-0 through M-9 to switch buffers rl_bind_keyseq ("C-p", rl_named_function ("previous-buffer")); rl_bind_keyseq ("C-n", rl_named_function ("next-buffer")); rl_bind_keyseq ("M-p", rl_named_function ("previous-history")); rl_bind_keyseq ("M-n", rl_named_function ("next-history")); rl_catch_sigwinch = false; ctx.readline_prompt_shown = true; rl_callback_handler_install (ctx.readline_prompt, on_readline_input); ctx.polling = true; while (ctx.polling) poller_run (&ctx.poller); if (ctx.readline_prompt_shown) rl_callback_handler_remove (); putchar ('\n'); app_context_free (&ctx); free_terminal (); return EXIT_SUCCESS; }