/* * 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 // Literally cancer #undef lines #undef columns #include #include // --- Configuration (application-specific) ------------------------------------ // TODO: reject all junk present in the configuration; there can be newlines static struct config_item g_config_table[] = { { "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" }, { "isolate_buffers", "off", "Isolate global/server buffers" }, { 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" }, { NULL, NULL, NULL } }; // --- Application data -------------------------------------------------------- // All text stored in our data structures is encoded in UTF-8. // Or at least should be. /// 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_PRIVMSG, ///< PRIVMSG BUFFER_LINE_ACTION, ///< PRIVMSG ACTION BUFFER_LINE_NOTICE, ///< NOTICE BUFFER_LINE_JOIN, ///< JOIN BUFFER_LINE_PART, ///< PART BUFFER_LINE_KICK, ///< KICK BUFFER_LINE_QUIT, ///< QUIT BUFFER_LINE_STATUS, ///< Whatever status messages BUFFER_LINE_ERROR ///< Whatever error messages }; struct buffer_line { LIST_HEADER (struct buffer_line) // We use the "type" and "flags" mostly just as formatting hints enum buffer_line_type type; ///< Type of the event int flags; ///< Flags time_t when; ///< Time of the event char *who; ///< Name of the origin or NULL (user) char *object; ///< Text of message, object of action char *reason; ///< Reason for PART, KICK, QUIT }; struct buffer_line * buffer_line_new (void) { struct buffer_line *self = xcalloc (1, sizeof *self); return self; } static void buffer_line_destroy (struct buffer_line *self) { free (self->who); free (self->object); free (self->reason); free (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 HISTORY_STATE *history; ///< Saved history state char *saved_line; ///< Saved line int saved_point; ///< Saved position in line int saved_mark; ///< Saved mark // Buffer contents: struct buffer_line *lines; ///< All lines in this buffer struct buffer_line *lines_tail; ///< The tail of buffer lines unsigned lines_count; ///< How many lines we have unsigned unseen_messages_count; ///< # messages since last visited // Channel information: char *mode; ///< Channel mode 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); // Can't really free "history" here free (self->saved_line); free (self->mode); 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 { // Configuration: 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 bool isolate_buffers; ///< Isolate global/server buffers // Server connection: 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 SSL_CTX *ssl_ctx; ///< SSL context SSL *ssl; ///< SSL connection // TODO: initialize and update these two values // TODO: probably issue a USERHOST message for ourselves after connecting // to enable proper word-wrapping; then store it here also, separately, // without the nickname at the beginning; also could just use WHOIS char *irc_nickname; ///< Current nickname char *irc_user_mode; ///< Current user mode // Events: 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 struct poller poller; ///< Manages polled descriptors bool quitting; ///< User requested quitting bool polling; ///< The event loop is running // Buffers: struct buffer *buffers; ///< All our buffers in order struct buffer *buffers_tail; ///< The tail of our buffers struct str_map buffers_by_name; ///< 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 // Terminal: iconv_t term_to_utf8; ///< Terminal encoding to UTF-8 iconv_t term_from_utf8; ///< UTF-8 to terminal encoding // XXX: shouldn't it be rather UTF-8 from Latin 1? iconv_t term_from_latin1; ///< ISO Latin 1 to terminal encoding int lines; ///< Current terminal height int columns; ///< Current ternimal width 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; str_map_init (&self->buffers_by_name); self->buffers_by_name.key_xfrm = irc_strxfrm; 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); free (self->irc_nickname); free (self->irc_user_mode); LIST_FOR_EACH (struct buffer, iter, self->buffers) buffer_destroy (iter); str_map_free (&self->buffers_by_name); poller_free (&self->poller); iconv_close (self->term_from_latin1); iconv_close (self->term_from_utf8); iconv_close (self->term_to_utf8); free (self->readline_prompt); } static void refresh_prompt (struct app_context *ctx); // --- 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; int saved_mark; }; static void app_readline_hide (struct app_readline_state *state) { state->saved_point = rl_point; state->saved_point = rl_mark; 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_mark = state->saved_mark; 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 buffer_send (struct app_context *ctx, struct buffer *buffer, enum buffer_line_type type, int flags, const char *origin, const char *reason, const char *format, ...) ATTRIBUTE_PRINTF (7, 8); static void buffer_update_time (struct app_context *ctx, time_t now) { struct tm last, current; if (!localtime_r (&ctx->last_displayed_msg_time, &last) || !localtime_r (&now, ¤t)) { // Strange but nonfatal print_error ("%s: %s", "localtime_r", strerror (errno)); return; } ctx->last_displayed_msg_time = now; if (last.tm_year == current.tm_year && last.tm_mon == current.tm_mon && last.tm_mday == current.tm_mday) return; char buf[32] = ""; if (soft_assert (strftime (buf, sizeof buf, "%F", ¤t))) print_status ("%s", buf); // Else the buffer was too small, which is pretty weird } static void buffer_line_display (struct app_context *ctx, struct buffer_line *line) { // Normal timestamps don't include the date, this way the user won't be // confused as to when an event has happened buffer_update_time (ctx, line->when); struct str text; str_init (&text); struct tm current; if (!localtime_r (&line->when, ¤t)) print_error ("%s: %s", "localtime_r", strerror (errno)); else str_append_printf (&text, "%02d:%02d:%02d ", current.tm_hour, current.tm_min, current.tm_sec); char *who = iconv_xstrdup (ctx->term_from_utf8, line->who, -1, NULL); char *object = iconv_xstrdup (ctx->term_from_utf8, line->object, -1, NULL); char *reason = iconv_xstrdup (ctx->term_from_utf8, line->reason, -1, NULL); // TODO: colorize the output, note that we shouldn't put everything through // tputs but only the attribute strings. That might prove a bit // challenging. Maybe we could create a helper object to pust text // and formatting into. We could have a varargs function to make it a bit // more friendly, e.g. push(&x, ATTR_JOIN, "--> ", ATTR_RESET, who, NULL) switch (line->type) { case BUFFER_LINE_PRIVMSG: str_append_printf (&text, "<%s> %s", who, object); break; case BUFFER_LINE_ACTION: str_append_printf (&text, " * %s %s", who, object); break; case BUFFER_LINE_NOTICE: str_append_printf (&text, " - Notice(%s): %s", who, object); break; case BUFFER_LINE_JOIN: if (who) str_append_printf (&text, "--> %s has joined %s", who, object); else str_append_printf (&text, "--> You have joined %s", object); break; case BUFFER_LINE_PART: if (who) str_append_printf (&text, "<-- %s has left %s (%s)", who, object, reason); else str_append_printf (&text, "<-- You have left %s (%s)", object, reason); break; case BUFFER_LINE_KICK: if (who) str_append_printf (&text, "<-- %s has kicked %s (%s)", who, object, reason); else str_append_printf (&text, "<-- You have kicked %s (%s)", object, reason); break; case BUFFER_LINE_QUIT: if (who) str_append_printf (&text, "<-- %s has quit (%s)", who, reason); else str_append_printf (&text, "<-- You have quit (%s)", reason); break; case BUFFER_LINE_STATUS: str_append_printf (&text, " - %s", object); break; case BUFFER_LINE_ERROR: str_append_printf (&text, "=!= %s", object); } free (who); free (object); free (reason); struct app_readline_state state; if (ctx->readline_prompt_shown) app_readline_hide (&state); // TODO: write the line to a log file; note that the global and server // buffers musn't collide with filenames printf ("%s\n", text.str); str_free (&text); if (ctx->readline_prompt_shown) app_readline_restore (&state, ctx->readline_prompt); } static void buffer_send (struct app_context *ctx, struct buffer *buffer, enum buffer_line_type type, int flags, const char *origin, const char *reason, const char *format, ...) { va_list ap; va_start (ap, format); struct str text; str_init (&text); str_append_vprintf (&text, format, ap); va_end (ap); struct buffer_line *line = buffer_line_new (); line->type = type; line->flags = flags; line->when = time (NULL); line->who = xstrdup (origin); line->object = str_steal (&text); line->reason = xstrdup (reason); LIST_APPEND_WITH_TAIL (buffer->lines, buffer->lines_tail, line); buffer->lines_count++; if (buffer == ctx->current_buffer) buffer_line_display (ctx, line); } static struct buffer * buffer_by_name (struct app_context *ctx, const char *name) { return str_map_find (&ctx->buffers_by_name, name); } static void buffer_add (struct app_context *ctx, struct buffer *buffer) { hard_assert (!buffer_by_name (ctx, buffer->name)); str_map_set (&ctx->buffers_by_name, buffer->name, buffer); LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer); // In theory this can't cause changes in the prompt refresh_prompt (ctx); } static void buffer_remove (struct app_context *ctx, struct buffer *buffer) { hard_assert (buffer != ctx->current_buffer); // rl_clear_history, being the only way I know of to get rid of the complete // history including attached data, is a pretty recent addition. *sigh* #if RL_READLINE_VERSION >= 0x0603 if (buffer->history) { // See buffer_activate() for why we need to do this BS rl_free_undo_list (); // This is probably the only way we can free the history fully HISTORY_STATE *state = history_get_history_state (); history_set_history_state (buffer->history); rl_clear_history (); history_set_history_state (state); free (state); } #endif // RL_READLINE_VERSION str_map_set (&ctx->buffers_by_name, buffer->name, NULL); LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer); buffer_destroy (buffer); // It's not a good idea to remove these buffers, but it's even a worse // one to leave the pointers point to invalid memory if (buffer == ctx->global_buffer) ctx->global_buffer = NULL; if (buffer == ctx->server_buffer) ctx->server_buffer = NULL; refresh_prompt (ctx); } static void buffer_activate (struct app_context *ctx, struct buffer *buffer) { if (ctx->current_buffer == buffer) return; print_status ("%s", buffer->name); // That is, minus the buffer switch line and the readline prompt int to_display = MAX (10, ctx->lines - 2); struct buffer_line *line = buffer->lines_tail; while (line && line->prev && --to_display > 0) line = line->prev; // Once we've found where we want to start with the backlog, print it for (; line; line = line->next) buffer_line_display (ctx, line); // The following part shows you why it's not a good idea to use // GNU Readline for this kind of software. Or for anything else, really. // There could possibly be occurences of the current undo list in some // history entry. We either need to free the undo list, or move it // somewhere else to load back later, as the buffer we're switching to // has its own history state. rl_free_undo_list (); // Save this buffer's history so that it's independent for each buffer if (ctx->current_buffer) { ctx->current_buffer->history = history_get_history_state (); ctx->current_buffer->saved_line = rl_copy_text (0, rl_end); ctx->current_buffer->saved_point = rl_point; ctx->current_buffer->saved_mark = rl_mark; } else // Just throw it away; there should always be an active buffer however #if RL_READLINE_VERSION >= 0x0603 rl_clear_history (); #else // RL_READLINE_VERSION < 0x0603 // At least something... this may leak undo entries clear_history (); #endif // RL_READLINE_VERSION < 0x0603 // Now at last we can switch the pointers ctx->current_buffer = buffer; // Restore the target buffer's history if (buffer->history) { // history_get_history_state() just allocates a new HISTORY_STATE // and fills it with its current internal data. We don't need that // shell anymore after reviving it. history_set_history_state (buffer->history); free (buffer->history); buffer->history = NULL; } else { // This should get us a clean history while keeping the flags. // Note that we've either saved the previous history entries, or we've // cleared them altogether, so there should be nothing to leak. HISTORY_STATE *state = history_get_history_state (); state->offset = state->length = state->size = 0; history_set_history_state (state); free (state); } // Try to restore the target buffer's readline state if (buffer->saved_line) { rl_replace_line (buffer->saved_line, 0); rl_point = buffer->saved_point; rl_mark = buffer->saved_mark; free (buffer->saved_line); if (ctx->readline_prompt_shown) rl_redisplay (); } refresh_prompt (ctx); } /// Activate the n-th buffer, counting from one static bool buffer_goto (struct app_context *ctx, int n) { int i = 0; LIST_FOR_EACH (struct buffer, iter, ctx->buffers) if (++i == n) { buffer_activate (ctx, iter); return true; } return false; } static int buffer_get_index (struct app_context *ctx, struct buffer *buffer) { int index = 1; LIST_FOR_EACH (struct buffer, iter, ctx->buffers) { if (iter == buffer) return index; index++; } return -1; } static void init_buffers (struct app_context *ctx) { // At the moment we have only two global everpresent buffers 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 // FIXME: print to the server buffer 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) // FIXME: print to the global buffer 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)) // FIXME: print to the global buffer 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; char *address = format_host_port_pair (real_host, port); // FIXME: print to the server buffer 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; } // --- More readline funky stuff ----------------------------------------------- static char * get_unseen_prefix (struct app_context *ctx) { struct str active_buffers; str_init (&active_buffers); size_t i = 0; LIST_FOR_EACH (struct buffer, iter, ctx->buffers) { i++; if (!iter->unseen_messages_count) continue; if (active_buffers.len) str_append_c (&active_buffers, ','); str_append_printf (&active_buffers, "%zu", i); } if (active_buffers.len) return str_steal (&active_buffers); str_free (&active_buffers); return NULL; } 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 if (soft_assert (ctx->current_buffer)) { struct buffer *buffer = ctx->current_buffer; str_append_c (&prompt, '['); char *unseen_prefix = get_unseen_prefix (ctx); if (unseen_prefix) str_append_printf (&prompt, "(%s) ", unseen_prefix); free (unseen_prefix); str_append_printf (&prompt, "%d:%s", buffer_get_index (ctx, buffer), buffer->name); if (buffer->type == BUFFER_CHANNEL && *buffer->mode) str_append_printf (&prompt, "(%s)", buffer->mode); if (buffer != ctx->global_buffer) { str_append_c (&prompt, ' '); str_append (&prompt, ctx->irc_nickname); if (*ctx->irc_user_mode) str_append_printf (&prompt, "(%s)", ctx->irc_user_mode); } 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); // We need to be somehow able to initialize it rl_set_prompt (ctx->readline_prompt); if (ctx->readline_prompt_shown) rl_redisplay (); } static int on_readline_goto_buffer (int count, int key) { (void) count; if (!(key & 0x80)) return 0; int n = (key & 0x7F) - '0'; if (n < 0 || n > 9) return 0; // There's no zero-th buffer if (n == 0) n = 10; struct app_context *ctx = g_ctx; if (!buffer_goto (ctx, n)) rl_ding (); return 0; } static int on_readline_previous_buffer (int count, int key) { (void) key; struct app_context *ctx = g_ctx; if (!ctx->current_buffer) return 0; struct buffer *new_buffer = ctx->current_buffer; while (count-- > 0) if (!(new_buffer = new_buffer->prev)) new_buffer = ctx->buffers_tail; buffer_activate (ctx, new_buffer); return 0; } static int on_readline_next_buffer (int count, int key) { (void) key; struct app_context *ctx = g_ctx; if (!ctx->current_buffer) return 0; struct buffer *new_buffer = ctx->current_buffer; while (count-- > 0) if (!(new_buffer = new_buffer->next)) new_buffer = ctx->buffers; buffer_activate (ctx, new_buffer); return 0; } static int init_readline (void) { // TODO: maybe use rl_make_bare_keymap() and start from there; // our dear user could potentionally rig things up in a way that might // result in some funny unspecified behaviour rl_add_defun ("previous-buffer", on_readline_previous_buffer, -1); rl_add_defun ("next-buffer", on_readline_next_buffer, -1); // Redefine M-0 through M-9 to switch buffers for (int i = 0; i <= 9; i++) rl_bind_key (0x80 /* this is the Meta modifier for Readline */ | ('0' + i), on_readline_goto_buffer); 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")); return 0; } // --- Input handling ---------------------------------------------------------- // TODO: we will need a proper mode parser; to be shared with kike // TODO: we alse definitely need to parse server capability messages static void irc_handle_ping (struct app_context *ctx, const struct irc_message *msg) { if (msg->params.len) irc_send (ctx, "PONG :%s", msg->params.vector[0]); else irc_send (ctx, "PONG"); } static struct irc_handler { char *name; void (*handler) (struct app_context *ctx, const struct irc_message *msg); } g_irc_handlers[] = { // This list needs to stay sorted // TODO: handle as much as we can { "PING", irc_handle_ping }, }; static int irc_handler_cmp_by_name (const void *a, const void *b) { const struct irc_handler *first = a; const struct irc_handler *second = b; return strcasecmp_ascii (first->name, second->name); } 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); } if (!ctx->irc_ready && (!strcasecmp (msg->command, "MODE") || !strcasecmp (msg->command, "376") // RPL_ENDOFMOTD || !strcasecmp (msg->command, "422"))) // ERR_NOMOTD { // FIXME: print to the server buffer 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); } struct irc_handler key = { .name = msg->command }; struct irc_handler *handler = bsearch (&key, g_irc_handlers, N_ELEMENTS (g_irc_handlers), sizeof key, irc_handler_cmp_by_name); if (handler) handler->handler (ctx, msg); // Numerics typically have human-readable information unsigned long dummy; if (xstrtoul (&dummy, msg->command, 10)) // TODO: ensure proper encoding // FIXME: print to the server buffer print_status ("%s", raw); } // --- User input handling ----------------------------------------------------- static void handle_command_help (struct app_context *, const char *); static void handle_command_buffer (struct app_context *ctx, const char *arguments) { // TODO: parse the arguments } static void handle_command_quit (struct app_context *ctx, const char *arguments) { if (ctx->irc_fd != -1) { if (*arguments) irc_send (ctx, "QUIT :%s", arguments); else irc_send (ctx, "QUIT :%s", PROGRAM_NAME " " PROGRAM_VERSION); } initiate_quit (ctx); } static struct command_handler { char *name; void (*handler) (struct app_context *ctx, const char *arguments); // TODO: probably also a usage string } g_command_handlers[] = { { "help", handle_command_help }, { "quit", handle_command_quit }, { "buffer", handle_command_buffer }, #if 0 { "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 }, #endif }; static void handle_command_help (struct app_context *ctx, const char *arguments) { // TODO: show a list of all user commands } static int command_handler_cmp_by_length (const void *a, const void *b) { const struct command_handler *first = a; const struct command_handler *second = b; return strlen (first->name) - strlen (second->name); } static void init_partial_matching_user_command_map (struct str_map *partial) { str_map_init (partial); partial->key_xfrm = tolower_ascii_strxfrm; // We process them from the longest to the shortest one, // so that common prefixes favor shorter entries struct command_handler *by_length[N_ELEMENTS (g_command_handlers)]; for (size_t i = 0; i < N_ELEMENTS (by_length); i++) by_length[i] = &g_command_handlers[i]; qsort (by_length, N_ELEMENTS (by_length), sizeof *by_length, command_handler_cmp_by_length); for (size_t i = N_ELEMENTS (by_length); i--; ) { char *copy = xstrdup (by_length[i]->name); for (size_t part = strlen (copy); part; part--) { copy[part] = '\0'; str_map_set (partial, copy, by_length[i]); } free (copy); } } static void process_user_command (struct app_context *ctx, char *command) { // Trivially create a partial matching map static bool initialized = false; struct str_map partial; if (!initialized) { init_partial_matching_user_command_map (&partial); initialized = true; } // TODO: cut a single word (strtok_r()?) // TODO: if it's a number, switch to the given buffer struct command_handler *handler = str_map_find (&partial, command); if (handler) // FIXME: pass arguments correctly handler->handler (ctx, ""); } static void send_message_to_current_buffer (struct app_context *ctx, char *message) { struct buffer *buffer = ctx->current_buffer; if (!buffer) { // TODO: print an error message to the global buffer return; } switch (buffer->type) { case BUFFER_GLOBAL: case BUFFER_SERVER: // TODO: print a message to the buffer that it's not a channel break; case BUFFER_CHANNEL: // TODO: send an IRC message to the channel break; case BUFFER_PM: // TODO: send an IRC message to the user break; } } 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"); else if (input[0] != '/') send_message_to_current_buffer (ctx, input); else if (input[1] == '/') send_message_to_current_buffer (ctx, input + 1); else process_user_command (ctx, input + 1); free (input); } // --- 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; // FIXME: print to the server buffer 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); // FIXME: print to the server buffer 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; // FIXME: print to the server buffer 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: // FIXME: print to the server buffer print_error ("reading from the IRC server failed"); disconnected = true; goto end; case IRC_READ_EOF: // FIXME: print to the server buffer print_status ("the IRC server closed the connection"); disconnected = true; goto end; case IRC_READ_OK: break; } if (buf->len >= (1 << 20)) { // FIXME: print to the server buffer 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); // FIXME: print to the server buffer 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; } // FIXME: print to the server buffer 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 (); rl_get_screen_size (&ctx->lines, &ctx->columns); } } 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 unescape_config (struct str_map *input, struct str_map *output, struct error **e) { struct error *error = NULL; struct str_map_iter iter; str_map_iter_init (&iter, input); while (str_map_iter_next (&iter)) { struct str value; str_init (&value); if (!unescape_string (iter.link->data, &value, &error)) { error_set (e, "error reading configuration: %s: %s", iter.link->key, error->message); error_free (error); return false; } str_map_set (output, iter.link->key, str_steal (&value)); } 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; bool success = read_config_file (&map, e) && unescape_config (&map, &ctx->config, e) && autofill_user_info (ctx, e); str_map_free (&map); if (!success) return false; if (!irc_get_boolean_from_config (ctx, "reconnect", &ctx->reconnect, e) || !irc_get_boolean_from_config (ctx, "isolate_buffers", &ctx->isolate_buffers, 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 (); // This can cause memory leaks, or maybe even a segfault. Funny, eh? stifle_history (HISTORY_LIMIT); setup_signal_handlers (); init_colors (&ctx); init_poller_events (&ctx); init_buffers (&ctx); ctx.current_buffer = ctx.global_buffer; refresh_prompt (&ctx); // TODO: connect asynchronously (first step towards multiple servers) // TODO: print load_config() errors to the global buffer, // switch buffers and print irc_connect() errors to the server buffer? struct error *e = NULL; if (!load_config (&ctx, &e) || !irc_connect (&ctx, &e)) { // FIXME: print to the global buffer print_error ("%s", e->message); error_free (e); exit (EXIT_FAILURE); } rl_startup_hook = init_readline; rl_catch_sigwinch = false; rl_callback_handler_install (ctx.readline_prompt, on_readline_input); rl_get_screen_size (&ctx.lines, &ctx.columns); ctx.readline_prompt_shown = true; 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; }