/* * 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. The exception is IRC identifiers. /// Shorthand to set an error and return failure from the function #define FAIL(...) \ BLOCK_START \ error_set (e, __VA_ARGS__); \ return false; \ BLOCK_END static void user_unref (void *p); static void channel_unref (void *p); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct user_channel { LIST_HEADER (struct user_channel) struct channel *channel; ///< Reference to channel }; static struct user_channel * user_channel_new (void) { struct user_channel *self = xcalloc (1, sizeof *self); return self; } static void user_channel_destroy (void *p) { struct user_channel *self = p; channel_unref (self->channel); free (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // We keep references to user information in channels and buffers, // as well as in the name lookup table. struct user { size_t ref_count; ///< Reference count // TODO: eventually a reference to the server char *nickname; ///< Literal nickname // TODO: write code to poll for the away status bool away; ///< User is away struct user_channel *channels; ///< Channels the user is on }; static struct user * user_new (void) { struct user *self = xcalloc (1, sizeof *self); self->ref_count = 1; return self; } static void user_destroy (struct user *self) { free (self->nickname); LIST_FOR_EACH (struct user_channel, iter, self->channels) user_channel_destroy (iter); free (self); } static struct user * user_ref (struct user *self) { self->ref_count++; return self; } static void user_unref (void *p) { struct user *self = p; if (!--self->ref_count) user_destroy (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct channel_user { LIST_HEADER (struct channel_user) struct user *user; ///< Reference to user char *modes; ///< Op/voice/... characters }; static struct channel_user * channel_user_new (void) { struct channel_user *self = xcalloc (1, sizeof *self); return self; } static void channel_user_destroy (void *p) { struct channel_user *self = p; user_unref (self->user); free (self->modes); free (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // We keep references to channels in their users and buffers, // as well as in the name lookup table. struct channel { size_t ref_count; ///< Reference count // TODO: eventually a reference to the server char *name; ///< Channel name char *mode; ///< Channel mode char *topic; ///< Channel topic struct channel_user *users; ///< Channel users }; static struct channel * channel_new (void) { struct channel *self = xcalloc (1, sizeof *self); self->ref_count = 1; return self; } static void channel_destroy (void *p) { struct channel *self = p; free (self->name); free (self->mode); free (self->topic); LIST_FOR_EACH (struct channel_user, iter, self->users) channel_user_destroy (iter); free (self); } static struct channel * channel_ref (struct channel *self) { self->ref_count++; return self; } static void channel_unref (void *p) { struct channel *self = p; if (!--self->ref_count) channel_destroy (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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_NICK, ///< NICK 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); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 // Readline state: 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 // Origin information: struct user *user; ///< Reference to user struct channel *channel; ///< Reference to channel // TODO: eventually a reference to the server for server buffers }; static struct buffer * buffer_new (void) { struct buffer *self = xcalloc (1, sizeof *self); return self; } static void buffer_destroy (struct buffer *self) { free (self->name); // Can't really free "history" here free (self->saved_line); LIST_FOR_EACH (struct buffer_line, iter, self->lines) buffer_line_destroy (iter); if (self->user) user_unref (self->user); if (self->channel) channel_unref (self->channel); 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: an output queue to prevent excess floods (this will be needed // especially for away status polling) // XXX: there can be buffers for non-existent users // TODO: user buffers rename on nick changes // TODO: move entries in "irc_buffer_map" and "irc_users" when that happens // TODO: user buffers may merge when an existing user renames to match // the name of a buffer for a non-existent user // TODO: initialize key_strxfrm according to server properties; // note that collisions may arise on reconnecting // TODO: when disconnected, get rid of all users everywhere; // maybe also broadcast all buffers about the disconnection event // TODO: when getting connected again, rejoin all current channels struct str_map irc_users; ///< IRC user data struct str_map irc_channels; ///< IRC channel data struct str_map irc_buffer_map; ///< Maps IRC identifiers to buffers struct user *irc_user; ///< Our own user char *irc_user_mode; ///< Our current user mode char *irc_user_host; ///< Our current user@host // 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 // XXX: when we go multiserver, there will be collisions // TODO: make buffer names unique like weechat does 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 iconv_t latin1_to_utf8; ///< ISO Latin 1 to UTF-8 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->irc_users); self->irc_users.free = user_unref; self->irc_users.key_xfrm = irc_strxfrm; str_map_init (&self->irc_channels); self->irc_channels.free = channel_unref; self->irc_channels.key_xfrm = irc_strxfrm; str_map_init (&self->irc_buffer_map); self->irc_buffer_map.key_xfrm = irc_strxfrm; poller_init (&self->poller); str_map_init (&self->buffers_by_name); self->buffers_by_name.key_xfrm = irc_strxfrm; self->last_displayed_msg_time = time (NULL); 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->latin1_to_utf8 = iconv_open ("UTF-8", "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); str_map_free (&self->irc_users); str_map_free (&self->irc_channels); str_map_free (&self->irc_buffer_map); user_unref (self->irc_user); free (self->irc_user_mode); free (self->irc_user_host); poller_free (&self->poller); LIST_FOR_EACH (struct buffer, iter, self->buffers) buffer_destroy (iter); str_map_free (&self->buffers_by_name); iconv_close (self->latin1_to_utf8); iconv_close (self->term_from_utf8); iconv_close (self->term_to_utf8); free (self->readline_prompt); } static void refresh_prompt (struct app_context *ctx); static char *irc_cut_nickname (const char *prefix); static const char *irc_find_userhost (const char *prefix); // --- 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_mark = 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); } static void app_readline_erase_to_bol (const char *prompt) { rl_set_prompt (""); rl_replace_line ("", 0); rl_point = rl_mark = 0; rl_redisplay (); rl_set_prompt (prompt); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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; 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) char *nick = NULL; const char *userhost = NULL; if (who) { nick = irc_cut_nickname (who); userhost = irc_find_userhost (who); } switch (line->type) { case BUFFER_LINE_PRIVMSG: str_append_printf (&text, "<%s> %s", nick, object); break; case BUFFER_LINE_ACTION: str_append_printf (&text, " * %s %s", nick, object); break; case BUFFER_LINE_NOTICE: str_append_printf (&text, " - Notice(%s): %s", nick, object); break; case BUFFER_LINE_JOIN: if (who) str_append_printf (&text, "--> %s (%s) has joined %s", nick, userhost, object); else str_append_printf (&text, "--> You have joined %s", object); break; case BUFFER_LINE_PART: if (who) str_append_printf (&text, "<-- %s (%s) has left %s (%s)", nick, userhost, 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)", nick, object, reason); else str_append_printf (&text, "<-- You have kicked %s (%s)", object, reason); break; case BUFFER_LINE_NICK: if (who) str_append_printf (&text, "<-- %s is now known as %s", nick, object); else str_append_printf (&text, "<-- You are now known as %s", object); break; case BUFFER_LINE_QUIT: if (who) str_append_printf (&text, "<-- %s (%s) has quit (%s)", nick, userhost, 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 (nick); 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 ? origin : ""); line->object = str_steal (&text); line->reason = xstrdup (reason ? reason : ""); LIST_APPEND_WITH_TAIL (buffer->lines, buffer->lines_tail, line); buffer->lines_count++; if (buffer == ctx->current_buffer) buffer_line_display (ctx, line); else if (!ctx->isolate_buffers && (buffer == ctx->global_buffer || buffer == ctx->server_buffer)) // TODO: show this in another color or something buffer_line_display (ctx, line); else { buffer->unseen_messages_count++; refresh_prompt (ctx); } } #define buffer_send_status(ctx, buffer, ...) \ buffer_send (ctx, buffer, BUFFER_LINE_STATUS, 0, NULL, NULL, __VA_ARGS__) #define buffer_send_error(ctx, buffer, ...) \ buffer_send (ctx, buffer, BUFFER_LINE_ERROR, 0, NULL, NULL, __VA_ARGS__) 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); // TODO: part from the channel if needed // 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); buffer->unseen_messages_count = 0; // 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 // 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 (); } // Now at last we can switch the pointers ctx->current_buffer = buffer; refresh_prompt (ctx); } static struct buffer * buffer_at_index (struct app_context *ctx, int n) { int i = 0; LIST_FOR_EACH (struct buffer, iter, ctx->buffers) if (++i == n) return iter; return NULL; } static struct buffer * buffer_next (struct app_context *ctx, int count) { struct buffer *new_buffer = ctx->current_buffer; while (count-- > 0) if (!(new_buffer = new_buffer->next)) new_buffer = ctx->buffers; return new_buffer; } static struct buffer * buffer_previous (struct app_context *ctx, int count) { struct buffer *new_buffer = ctx->current_buffer; while (count-- > 0) if (!(new_buffer = new_buffer->prev)) new_buffer = ctx->buffers_tail; return new_buffer; } static bool buffer_goto (struct app_context *ctx, int n) { struct buffer *buffer = buffer_at_index (ctx, n); if (!buffer) return false; buffer_activate (ctx, buffer); return true; } 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); } // --- Users, channels --------------------------------------------------------- // --- Supporting code --------------------------------------------------------- static char * irc_cut_nickname (const char *prefix) { return xstrndup (prefix, strcspn (prefix, "!@")); } static const char * irc_find_userhost (const char *prefix) { const char *p = strchr (prefix, '!'); return p ? p + 1 : NULL; } static bool irc_is_this_us (struct app_context *ctx, const char *prefix) { char *nick = irc_cut_nickname (prefix); bool result = !irc_strcmp (nick, ctx->irc_user->nickname); free (nick); return result; } static bool irc_is_channel (struct app_context *ctx, const char *ident) { (void) ctx; // TODO: parse prefixes from server features return *ident && !!strchr ("#&+!", *ident); } 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) { // First get rid of readline if (ctx->readline_prompt_shown) { app_readline_erase_to_bol (ctx->readline_prompt); ctx->readline_prompt_shown = false; } // This is okay as long as we're not called from within readline rl_callback_handler_remove (); // Initiate a connection close buffer_send_status (ctx, ctx->global_buffer, "Shutting down"); if (ctx->irc_fd != -1) // XXX: when we go async, we'll have to flush output buffers first 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 buffer_send_error (ctx, ctx->server_buffer, "%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) buffer_send_error (ctx, ctx->global_buffer, "%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)) buffer_send_error (ctx, ctx->global_buffer, "%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); buffer_send_status (ctx, ctx->server_buffer, "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 * make_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 make_prompt (struct app_context *ctx, struct str *output) { struct buffer *buffer = ctx->current_buffer; if (!soft_assert (buffer)) return; str_append_c (output, '['); char *unseen_prefix = make_unseen_prefix (ctx); if (unseen_prefix) str_append_printf (output, "(%s) ", unseen_prefix); free (unseen_prefix); str_append_printf (output, "%d:%s", buffer_get_index (ctx, buffer), buffer->name); if (buffer->type == BUFFER_CHANNEL && *buffer->channel->mode) str_append_printf (output, "(%s)", buffer->channel->mode); if (buffer != ctx->global_buffer) { str_append_c (output, ' '); if (!ctx->irc_fd == -1) str_append (output, "(disconnected)"); else { str_append (output, ctx->irc_user->nickname); if (*ctx->irc_user_mode) str_append_printf (output, "(%s)", ctx->irc_user_mode); } } str_append_c (output, ']'); } static void refresh_prompt (struct app_context *ctx) { bool have_attributes = !!get_attribute_printer (stdout); struct str prompt; str_init (&prompt); make_prompt (ctx, &prompt); str_append_c (&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); // First reset the prompt to work around a bug in readline rl_set_prompt (""); if (ctx->readline_prompt_shown) rl_redisplay (); 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; int n = UNMETA (key) - '0'; if (n < 0 || n > 9) return 0; struct app_context *ctx = g_ctx; if (!buffer_goto (ctx, n == 0 ? 10 : 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) buffer_activate (ctx, buffer_previous (ctx, count)); return 0; } static int on_readline_next_buffer (int count, int key) { (void) key; struct app_context *ctx = g_ctx; if (ctx->current_buffer) buffer_activate (ctx, buffer_next (ctx, count)); return 0; } static int on_readline_return (int count, int key) { (void) count; (void) key; struct app_context *ctx = g_ctx; // Let readline pass the line to our input handler rl_done = 1; // Save readline state int saved_point = rl_point; int saved_mark = rl_mark; char *saved_line = rl_copy_text (0, rl_end); // Erase the entire line from screen rl_set_prompt (""); rl_replace_line ("", 0); rl_redisplay (); ctx->readline_prompt_shown = false; // Restore readline state rl_set_prompt (ctx->readline_prompt); rl_replace_line (saved_line, 0); rl_point = saved_point; rl_mark = saved_mark; free (saved_line); return 0; } static void app_readline_bind_meta (char key, rl_command_func_t cb) { // One of these is going to work char keyseq[] = { '\\', 'e', key, 0 }; rl_bind_key (META (key), cb); rl_bind_keyseq (keyseq, cb); } static int init_readline (void) { // XXX: 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++) app_readline_bind_meta ('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")); app_readline_bind_meta ('p', rl_named_function ("previous-history")); app_readline_bind_meta ('n', rl_named_function ("next-history")); // We need to hide the prompt first rl_bind_key (RETURN, on_readline_return); 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 char * irc_to_utf8 (struct app_context *ctx, const char *text) { size_t len = strlen (text) + 1; if (utf8_validate (text, len)) return xstrdup (text); return iconv_xstrdup (ctx->latin1_to_utf8, (char *) text, len, NULL); } static void irc_handle_join (struct app_context *ctx, const struct irc_message *msg) { if (!msg->prefix || msg->params.len < 1) return; const char *target = msg->params.vector[0]; if (!irc_is_channel (ctx, target)) return; struct channel *channel = str_map_find (&ctx->irc_channels, target); struct buffer *buffer = str_map_find (&ctx->irc_buffer_map, target); hard_assert ((channel && buffer) || (channel && !buffer) || (!channel && !buffer)); // We've joined a new channel if (!channel && irc_is_this_us (ctx, msg->prefix)) { channel = channel_new (); channel->name = xstrdup (target); channel->mode = xstrdup (""); channel->topic = NULL; str_map_set (&ctx->irc_channels, channel->name, channel); buffer = buffer_new (); buffer->type = BUFFER_CHANNEL; buffer->name = xstrdup (target); buffer->channel = channel_ref (channel); LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer); str_map_set (&ctx->irc_buffer_map, channel->name, buffer); buffer_activate (ctx, buffer); } // This is weird, ignoring if (!channel) return; // Get or make a user object char *nickname = irc_cut_nickname (msg->prefix); struct user *user = str_map_find (&ctx->irc_users, nickname); if (!user) { user = user_new (); user->nickname = nickname; str_map_set (&ctx->irc_users, user->nickname, user); } else free (nickname); // Link the user with the channel struct user_channel *user_channel = user_channel_new (); user_channel->channel = channel_ref (channel); LIST_PREPEND (user->channels, user_channel); struct channel_user *channel_user = channel_user_new (); channel_user->user = user_ref (user); channel_user->modes = xstrdup (""); LIST_PREPEND (channel->users, channel_user); // Finally log the message if (buffer) { buffer_send (ctx, buffer, BUFFER_LINE_JOIN, 0, msg->prefix, NULL, "%s", target); } } static void irc_handle_kick (struct app_context *ctx, const struct irc_message *msg) { // TODO: remove user from the channel // TODO: log a message } static void irc_handle_mode (struct app_context *ctx, const struct irc_message *msg) { // TODO: parse the mode change and apply it // TODO: log a message } static void irc_handle_nick (struct app_context *ctx, const struct irc_message *msg) { if (!msg->prefix || msg->params.len < 1) return; char *nickname = irc_cut_nickname (msg->prefix); struct user *user = str_map_find (&ctx->irc_users, nickname); free (nickname); if (!user) return; const char *new_nickname = msg->params.vector[0]; if (irc_is_this_us (ctx, msg->prefix)) { // Log a message in all open buffers on this server struct str_map_iter iter; str_map_iter_init (&iter, &ctx->irc_buffer_map); struct buffer *buffer; while ((buffer = str_map_iter_next (&iter))) { buffer_send (ctx, buffer, BUFFER_LINE_NICK, 0, NULL, NULL, "%s", new_nickname); } } else { // Log a message in any PM buffer struct buffer *buffer = str_map_find (&ctx->irc_buffer_map, user->nickname); if (buffer) { buffer_send (ctx, buffer, BUFFER_LINE_NICK, 0, msg->prefix, NULL, "%s", new_nickname); // TODO: rename the buffer, and if it collides, merge them } // Log a message in all channels the user is in LIST_FOR_EACH (struct user_channel, iter, user->channels) { buffer = str_map_find (&ctx->irc_buffer_map, iter->channel->name); hard_assert (buffer != NULL); buffer_send (ctx, buffer, BUFFER_LINE_NICK, 0, msg->prefix, NULL, "%s", new_nickname); } } // Finally rename the user free (user->nickname); user->nickname = xstrdup (new_nickname); // We might have renamed ourselves refresh_prompt (ctx); } static void irc_handle_notice (struct app_context *ctx, const struct irc_message *msg) { // TODO: log a message } static void irc_handle_part (struct app_context *ctx, const struct irc_message *msg) { // TODO: remove user from the channel // TODO: log a message } 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 void irc_handle_privmsg (struct app_context *ctx, const struct irc_message *msg) { if (!msg->prefix || msg->params.len < 2) return; const char *target = msg->params.vector[0]; const char *message = msg->params.vector[1]; struct buffer *buffer = str_map_find (&ctx->irc_buffer_map, target); if (irc_is_channel (ctx, target)) { struct channel *channel = str_map_find (&ctx->irc_channels, target); hard_assert ((channel && buffer) || (channel && !buffer) || (!channel && !buffer)); // This is weird, ignoring if (!channel) return; } else if (!buffer) { // Get or make a user object char *nickname = irc_cut_nickname (msg->prefix); struct user *user = str_map_find (&ctx->irc_users, nickname); if (!user) { user = user_new (); user->nickname = xstrdup (nickname); str_map_set (&ctx->irc_users, user->nickname, user); } // Open a new buffer for the user buffer = buffer_new (); buffer->type = BUFFER_PM; buffer->name = xstrdup (nickname); buffer->user = user_ref (user); LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer); str_map_set (&ctx->irc_buffer_map, user->nickname, buffer); free (nickname); } if (buffer) { // TODO: handle CTCP messages buffer_send (ctx, buffer, BUFFER_LINE_PRIVMSG, 0, msg->prefix, NULL, "%s", message); } } static void irc_handle_quit (struct app_context *ctx, const struct irc_message *msg) { if (!msg->prefix) return; // What the fuck if (irc_is_this_us (ctx, msg->prefix)) return; char *nickname = irc_cut_nickname (msg->prefix); struct user *user = str_map_find (&ctx->irc_users, nickname); free (nickname); if (!user) return; const char *message = NULL; if (msg->params.len > 0) message = msg->params.vector[0]; // Log a message in any PM buffer struct buffer *buffer = str_map_find (&ctx->irc_buffer_map, user->nickname); if (buffer) { buffer_send (ctx, buffer, BUFFER_LINE_QUIT, 0, msg->prefix, message, ""); // TODO: set some kind of a flag in the buffer and when the user // reappers on a channel (JOIN), log a "is back online" message. // Also set this flag when we receive a "no such nick" numeric // and reset it when we send something to the buffer. } // Log a message in all channels the user is in LIST_FOR_EACH (struct user_channel, iter, user->channels) { buffer = str_map_find (&ctx->irc_buffer_map, iter->channel->name); hard_assert (buffer != NULL); buffer_send (ctx, buffer, BUFFER_LINE_QUIT, 0, msg->prefix, message, ""); // Unlink the user from the channel struct channel *channel = iter->channel; LIST_FOR_EACH (struct channel_user, iter, channel->users) if (iter->user == user) { LIST_UNLINK (channel->users, iter); channel_user_destroy (iter); } LIST_UNLINK (user->channels, iter); user_channel_destroy (iter); } // If it's the last reference, there's no reason for the user to stay. // Note that when the user has their own buffer, it still keeps a reference // and we don't have to care about removing them from "irc_buffer_map". if (user->ref_count == 1) { hard_assert (!user->channels); str_map_set (&ctx->irc_users, user->nickname, NULL); } } static void irc_handle_topic (struct app_context *ctx, const struct irc_message *msg) { // TODO: log a message } 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 { "JOIN", irc_handle_join }, { "KICK", irc_handle_kick }, { "MODE", irc_handle_mode }, { "NICK", irc_handle_nick }, { "NOTICE", irc_handle_notice }, { "PART", irc_handle_part }, { "PING", irc_handle_ping }, { "PRIVMSG", irc_handle_privmsg }, { "QUIT", irc_handle_quit }, { "TOPIC", irc_handle_topic }, }; 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); char *utf8 = irc_to_utf8 (ctx, raw); fprintf (stderr, "[IRC] ==> \"%s\"\n", utf8); free (utf8); if (ctx->readline_prompt_shown) app_readline_restore (&state, ctx->readline_prompt); } // XXX: or is the 001 numeric enough? For what? if (!ctx->irc_ready && (!strcasecmp (msg->command, "MODE") || !strcasecmp (msg->command, "376") // RPL_ENDOFMOTD || !strcasecmp (msg->command, "422"))) // ERR_NOMOTD { // XXX: should we really print this? buffer_send_status (ctx, ctx->server_buffer, "Successfully connected"); ctx->irc_ready = true; refresh_prompt (ctx); // TODO: parse any response and store the result for us in app_context; // this enables proper message splitting on output; // we can also use WHOIS if it's not supported (optional by RFC 2812) irc_send (ctx, "USERHOST %s", ctx->irc_user->nickname); 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); return; } // Numerics typically have human-readable information unsigned long dummy; if (xstrtoul (&dummy, msg->command, 10)) { // Get rid of the first parameter, if there's any at all, // as it contains our nickname and is of no practical use to the user struct str_vector copy; str_vector_init (©); str_vector_add_vector (©, msg->params.vector + !!msg->params.len); // Join the parameter vector back, recode it to our internal encoding // and send it to the server buffer char *reconstructed = join_str_vector (©, ' '); str_vector_free (©); char *utf8 = irc_to_utf8 (ctx, reconstructed); free (reconstructed); buffer_send_status (ctx, ctx->server_buffer, "%s", utf8); free (utf8); } } // --- User input handling ----------------------------------------------------- static void handle_command_help (struct app_context *, char *); /// Cuts the longest non-whitespace portion of text and advances the pointer static char * cut_word (char **s) { char *start = *s; size_t word_len = strcspn (*s, " \t"); char *end = start + word_len; *s = end + strspn (end, " \t"); *end = '\0'; return start; } static bool try_handle_buffer_goto (struct app_context *ctx, const char *word) { unsigned long n; if (!xstrtoul (&n, word, 10)) return false; if (n > INT_MAX || !buffer_goto (ctx, n)) buffer_send_error (ctx, ctx->global_buffer, "%s: %s", "no such buffer", word); return true; } static struct buffer * try_decode_buffer (struct app_context *ctx, const char *word) { unsigned long n; struct buffer *buffer = NULL; if (xstrtoul (&n, word, 10) && n <= INT_MAX) buffer = buffer_at_index (ctx, n); if (!buffer) buffer = buffer_by_name (ctx, word); // TODO: decode the global and server buffers, partial matches return buffer; } static void handle_command_buffer (struct app_context *ctx, char *arguments) { char *action = cut_word (&arguments); if (try_handle_buffer_goto (ctx, action)) return; struct buffer *buffer = NULL; // XXX: also build a prefix map? // It looks like we'll want to split this into functions anyway. // TODO: some subcommand to print N last lines from the buffer if (!strcasecmp_ascii (action, "list")) { buffer_send_status (ctx, ctx->global_buffer, "Buffers list:"); int i = 1; LIST_FOR_EACH (struct buffer, iter, ctx->buffers) buffer_send_status (ctx, ctx->global_buffer, " [%d] %s", i++, iter->name); } else if (!strcasecmp_ascii (action, "clear")) { // TODO } else if (!strcasecmp_ascii (action, "move")) { // TODO: unlink the buffer and link it back at index; // we will probably need to extend liberty for this } else if (!strcasecmp_ascii (action, "close")) { const char *which = NULL; if (!*arguments) buffer = ctx->current_buffer; else buffer = try_decode_buffer (ctx, (which = cut_word (&arguments))); if (!buffer) { buffer_send_error (ctx, ctx->global_buffer, "%s: %s", "No such buffer", which); return; } if (buffer == ctx->global_buffer) { buffer_send_error (ctx, ctx->global_buffer, "Can't close the global buffer"); return; } if (buffer == ctx->server_buffer) { buffer_send_error (ctx, ctx->global_buffer, "Can't close the server buffer"); return; } if (buffer == ctx->current_buffer) buffer_activate (ctx, buffer_next (ctx, 1)); buffer_remove (ctx, buffer); } else { // TODO: show usage (or do something else?) } } static void handle_command_quit (struct app_context *ctx, 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 void handle_command_join (struct app_context *ctx, char *arguments) { if (ctx->current_buffer->type == BUFFER_GLOBAL) { buffer_send_error (ctx, ctx->current_buffer, "Can't join from a global buffer"); return; } if (ctx->irc_fd == -1) { buffer_send_error (ctx, ctx->server_buffer, "Not connected"); return; } if (*arguments) // TODO: check if the arguments are in the form of // "channel(,channel)* key(,key)*" irc_send (ctx, "JOIN %s", arguments); else { if (ctx->current_buffer->type != BUFFER_CHANNEL) buffer_send_error (ctx, ctx->current_buffer, "%s: %s", "Can't join", "no argument given and this buffer is not a channel"); // TODO: have a better way of checking if we're on the channel else if (ctx->current_buffer->channel->users) buffer_send_error (ctx, ctx->current_buffer, "%s: %s", "Can't join", "you already are on the channel"); else // TODO: send the key if known irc_send (ctx, "JOIN %s", ctx->current_buffer->channel->name); } } static void handle_command_part (struct app_context *ctx, char *arguments) { if (ctx->current_buffer->type == BUFFER_GLOBAL) { buffer_send_error (ctx, ctx->current_buffer, "Can't part from a global buffer"); return; } if (ctx->irc_fd == -1) { buffer_send_error (ctx, ctx->server_buffer, "Not connected"); return; } if (*arguments) // TODO: check if the arguments are in the form of "channel(,channel)*" irc_send (ctx, "PART %s", arguments); else { if (ctx->current_buffer->type != BUFFER_CHANNEL) buffer_send_error (ctx, ctx->current_buffer, "%s: %s", "Can't part", "no argument given and this buffer is not a channel"); // TODO: have a better way of checking if we're on the channel else if (!ctx->current_buffer->channel->users) buffer_send_error (ctx, ctx->current_buffer, "%s: %s", "Can't join", "you're not on the channel"); else irc_send (ctx, "PART %s", ctx->current_buffer->channel->name); } } static void handle_command_quote (struct app_context *ctx, char *arguments) { if (ctx->current_buffer->type == BUFFER_GLOBAL) buffer_send_error (ctx, ctx->current_buffer, "Can't do this from a global buffer"); else irc_send (ctx, arguments); } static struct command_handler { const char *name; void (*handler) (struct app_context *ctx, char *arguments); const char *description; const char *usage; } g_command_handlers[] = { { "help", handle_command_help, "Show help", "[command]" }, { "quit", handle_command_quit, "Quit the program", "[message]" }, { "buffer", handle_command_buffer, "Manage buffers", "list | clear | move | { close [ | ] } | " }, #if 0 { "msg", NULL, "", "" }, { "query", NULL, "", "" }, { "notice", NULL, "", "" }, { "ctcp", NULL, "", "" }, { "me", NULL, "", "" }, #endif { "join", handle_command_join, "Join channels", "[channel...]" }, { "part", handle_command_part, "Leave channels", "[channel...]" }, #if 0 { "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, "", "" }, #endif { "quote", handle_command_quote, "Send a raw command to the server", "command" }, }; static void handle_command_help (struct app_context *ctx, char *arguments) { if (*arguments) { char *command = cut_word (&arguments); // TODO: search for the command and show specific help return; } buffer_send_status (ctx, ctx->global_buffer, "Commands:"); for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++) { struct command_handler *iter = &g_command_handlers[i]; buffer_send_status (ctx, ctx->global_buffer, " %s: %s", iter->name, iter->description); buffer_send_status (ctx, ctx->global_buffer, " Arguments: %s", iter->usage); } } 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) { // Trivially create a partial matching map 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) { static bool initialized = false; static struct str_map partial; if (!initialized) { init_partial_matching_user_command_map (&partial); initialized = true; } char *name = cut_word (&command); if (try_handle_buffer_goto (ctx, name)) return; struct command_handler *handler = str_map_find (&partial, name); if (handler) handler->handler (ctx, command); else buffer_send_error (ctx, ctx->global_buffer, "%s: %s", "No such command", name); } static void send_message_to_target (struct app_context *ctx, const char *target, char *message, struct buffer *buffer) { if (ctx->irc_fd == -1) { buffer_send_error (ctx, buffer, "Not connected"); return; } // TODO: autosplit irc_send (ctx, "PRIVMSG %s :%s", target, message); buffer_send (ctx, buffer, BUFFER_LINE_PRIVMSG, 0, ctx->irc_user->nickname, NULL, "%s", message); } static void send_message_to_current_buffer (struct app_context *ctx, char *message) { struct buffer *buffer = ctx->current_buffer; hard_assert (buffer != NULL); switch (buffer->type) { case BUFFER_GLOBAL: case BUFFER_SERVER: buffer_send_error (ctx, buffer, "This buffer is not a channel"); break; case BUFFER_CHANNEL: send_message_to_target (ctx, buffer->channel->name, message, buffer); break; case BUFFER_PM: send_message_to_target (ctx, buffer->user->nickname, message, buffer); 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; buffer_send_error (ctx, ctx->server_buffer, "%s", e->message); error_free (e); irc_queue_reconnect (ctx); } static void irc_queue_reconnect (struct app_context *ctx) { // TODO: exponentional backoff hard_assert (ctx->irc_fd == -1); buffer_send_status (ctx, ctx->server_buffer, "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; user_unref (ctx->irc_user); ctx->irc_user = NULL; free (ctx->irc_user_mode); ctx->irc_user_mode = NULL; free (ctx->irc_user_host); ctx->irc_user_host = NULL; 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) // XXX: not sure if we want this in a client initiate_quit (ctx); else irc_queue_reconnect (ctx); } static void on_irc_ping_timeout (void *user_data) { struct app_context *ctx = user_data; buffer_send_error (ctx, ctx->server_buffer, "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: buffer_send_error (ctx, ctx->server_buffer, "Reading from the IRC server failed"); disconnected = true; goto end; case IRC_READ_EOF: buffer_send_error (ctx, ctx->server_buffer, "The IRC server closed the connection"); disconnected = true; goto end; case IRC_READ_OK: break; } if (buf->len >= (1 << 20)) { buffer_send_error (ctx, ctx->server_buffer, "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? 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); buffer_send_status (ctx, ctx->server_buffer, "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; } buffer_send_status (ctx, ctx->server_buffer, "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); // XXX: maybe we should wait for the first message from the server ctx->irc_user = user_new (); ctx->irc_user->nickname = xstrdup (nickname); ctx->irc_user_mode = xstrdup (""); ctx->irc_user_host = NULL; 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) { if (line) { if (*line) add_history (line); process_input (g_ctx, line); free (line); } else { app_readline_erase_to_bol (g_ctx->readline_prompt); rl_ding (); } // initiate_quit() disables readline; we just wait then if (!g_ctx->quitting) 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; // 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, "realname", 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; const char *irc_host = str_map_find (&ctx->config, "irc_host"); if (!irc_host) { error_set (e, "no hostname specified in configuration"); 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); print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting"); // We only need to convert to and from the terminal encoding setlocale (LC_CTYPE, ""); struct app_context ctx; app_context_init (&ctx); g_ctx = &ctx; 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 (); struct error *e = NULL; if (!load_config (&ctx, &e)) { print_error ("%s", e->message); error_free (e); exit (EXIT_FAILURE); } 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) if (!irc_connect (&ctx, &e)) { buffer_send_error (&ctx, ctx.server_buffer, "%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); app_context_free (&ctx); free_terminal (); return EXIT_SUCCESS; }