diff --git a/degesch.c b/degesch.c new file mode 100644 index 0000000..02af1d9 --- /dev/null +++ b/degesch.c @@ -0,0 +1,1707 @@ +/* + * degesch.c: the experimental IRC client + * + * Copyright (c) 2015, Přemysl Janouch + * All rights reserved. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +/// Some arbitrary limit for the history file +#define HISTORY_LIMIT 10000 + +// String constants for all attributes we use for output +#define ATTR_PROMPT "attr_prompt" +#define ATTR_RESET "attr_reset" +#define ATTR_WARNING "attr_warning" +#define ATTR_ERROR "attr_error" + +// User data for logger functions to enable formatted logging +#define print_fatal_data ATTR_ERROR +#define print_error_data ATTR_ERROR +#define print_warning_data ATTR_WARNING + +#include "config.h" +#undef PROGRAM_NAME +#define PROGRAM_NAME "degesch" + +#include "common.c" + +#include +#include +#include + +#include +#include +#include +#include + +// --- Configuration (application-specific) ------------------------------------ + +static struct config_item g_config_table[] = +{ + { ATTR_PROMPT, NULL, "Terminal attributes for the prompt" }, + { ATTR_RESET, NULL, "String to reset terminal attributes" }, + { ATTR_WARNING, NULL, "Terminal attributes for warnings" }, + { ATTR_ERROR, NULL, "Terminal attributes for errors" }, + + { "nickname", NULL, "IRC nickname" }, + { "username", NULL, "IRC user name" }, + { "realname", NULL, "IRC real name/e-mail" }, + + { "irc_host", NULL, "Address of the IRC server" }, + { "irc_port", "6667", "Port of the IRC server" }, + { "ssl", "off", "Whether to use SSL" }, + { "ssl_cert", NULL, "Client SSL certificate (PEM)" }, + { "ssl_verify", "on", "Whether to verify certificates" }, + { "ssl_ca_file", NULL, "OpenSSL CA bundle file" }, + { "ssl_ca_path", NULL, "OpenSSL CA bundle path" }, + { "autojoin", NULL, "Channels to join on start" }, + { "reconnect", "on", "Whether to reconnect on error" }, + { "reconnect_delay", "5", "Time between reconnecting" }, + + { "socks_host", NULL, "Address of a SOCKS 4a/5 proxy" }, + { "socks_port", "1080", "SOCKS port number" }, + { "socks_username", NULL, "SOCKS auth. username" }, + { "socks_password", NULL, "SOCKS auth. password" }, + + { NULL, NULL, NULL } +}; + +// --- Application data -------------------------------------------------------- + +/// Shorthand to set an error and return failure from the function +#define FAIL(...) \ + BLOCK_START \ + error_set (e, __VA_ARGS__); \ + return false; \ + BLOCK_END + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +enum buffer_line_flags +{ + BUFFER_LINE_HIGHLIGHT = 1 << 0 ///< The user was highlighted by this +}; + +enum buffer_line_type +{ + BUFFER_LINE_TEXT, ///< PRIVMSG + BUFFER_LINE_NOTICE, ///< NOTICE + BUFFER_LINE_STATUS ///< JOIN, PART, QUIT +}; + +struct buffer_line +{ + LIST_HEADER (struct buffer_line) + + enum buffer_line_type type; ///< Type of the event + int flags; ///< Flags + + time_t when; ///< Time of the event + char *origin; ///< Name of the origin + char *text; ///< The text of the message +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct nick_info +{ + char *nickname; ///< Literal nickname + char mode_char; ///< Op/voice/... character + bool away; ///< User is away + + // XXX: maybe a good candidate for deduplication (away status) +}; + +static void +nick_info_destroy (void *p) +{ + struct nick_info *self = p; + free (self->nickname); + free (self); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +enum buffer_type +{ + BUFFER_GLOBAL, ///< Global information + BUFFER_SERVER, ///< Server-related messages + BUFFER_CHANNEL, ///< Channels + BUFFER_PM ///< Private messages (query) +}; + +struct buffer +{ + LIST_HEADER (struct buffer) + + enum buffer_type type; ///< Type of the buffer + char *name; ///< The name of the buffer + + unsigned unseen_messages; ///< # messages since last visited + + // TODO: now I can't just print messages with print_status() etc., + // all that stuff has to go into a buffer now + + // Channels: + + char *topic; ///< Channel topic + struct str_map nicks; ///< Maps nicks to "nick_info" +}; + +static struct buffer * +buffer_new (void) +{ + struct buffer *self = xcalloc (1, sizeof *self); + str_map_init (&self->nicks); + self->nicks.key_xfrm = irc_strxfrm; + self->nicks.free = nick_info_destroy; + return self; +} + +static void +buffer_destroy (struct buffer *self) +{ + free (self->name); + free (self->topic); + str_map_free (&self->nicks); + free (self); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +enum color_mode +{ + COLOR_AUTO, ///< Autodetect if colours are available + COLOR_ALWAYS, ///< Always use coloured output + COLOR_NEVER ///< Never use coloured output +}; + +struct app_context +{ + struct str_map config; ///< User configuration + enum color_mode color_mode; ///< Colour output mode + bool reconnect; ///< Whether to reconnect on conn. fail. + unsigned long reconnect_delay; ///< Reconnect delay in seconds + + int irc_fd; ///< Socket FD of the server + struct str read_buffer; ///< Input yet to be processed + struct poller_fd irc_event; ///< IRC FD event + bool irc_ready; ///< Whether we may send messages now + + struct poller_fd tty_event; ///< Terminal input event + struct poller_fd signal_event; ///< Signal FD event + struct poller_timer ping_tmr; ///< We should send a ping + struct poller_timer timeout_tmr; ///< Connection seems to be dead + struct poller_timer reconnect_tmr; ///< We should reconnect now + + SSL_CTX *ssl_ctx; ///< SSL context + SSL *ssl; ///< SSL connection + + struct buffer *buffers; ///< All our buffers in order + struct buffer *buffers_tail; ///< The tail of our buffers + + // TODO: a name -> buffer map that excludes GLOBAL and SERVER + struct buffer *global_buffer; ///< The global buffer + struct buffer *server_buffer; ///< The server buffer + + struct buffer *current_buffer; ///< The current buffer + + // TODO: So that we always output proper date change messages + time_t last_displayed_msg_time; ///< Time of last displayed message + + struct poller poller; ///< Manages polled descriptors + bool quitting; ///< User requested quitting + bool polling; ///< The event loop is running + + iconv_t term_to_utf8; ///< Terminal encoding to UTF-8 + iconv_t term_from_utf8; ///< UTF-8 to terminal encoding + iconv_t term_from_latin1; ///< ISO Latin 1 to terminal encoding + + char *readline_prompt; ///< The prompt we use for readline + bool readline_prompt_shown; ///< Whether the prompt is shown now +} +*g_ctx; + +static void on_irc_ping_timeout (void *user_data); +static void on_irc_timeout (void *user_data); +static void on_irc_reconnect_timeout (void *user_data); + +static void +app_context_init (struct app_context *self) +{ + memset (self, 0, sizeof *self); + + str_map_init (&self->config); + self->config.free = free; + load_config_defaults (&self->config, g_config_table); + + self->irc_fd = -1; + str_init (&self->read_buffer); + self->irc_ready = false; + + self->last_displayed_msg_time = time (NULL); + + poller_init (&self->poller); + + char *encoding = nl_langinfo (CODESET); +#ifdef __linux__ + encoding = xstrdup_printf ("%s//TRANSLIT", encoding); +#else // ! __linux__ + encoding = xstrdup (encoding); +#endif // ! __linux__ + + if ((self->term_from_utf8 = + iconv_open (encoding, "UTF-8")) == (iconv_t) -1 + || (self->term_from_latin1 = + iconv_open (encoding, "ISO-8859-1")) == (iconv_t) -1 + || (self->term_to_utf8 = + iconv_open ("UTF-8", nl_langinfo (CODESET))) == (iconv_t) -1) + exit_fatal ("creating the UTF-8 conversion object failed: %s", + strerror (errno)); + + free (encoding); +} + +static void +app_context_free (struct app_context *self) +{ + str_map_free (&self->config); + str_free (&self->read_buffer); + + if (self->irc_fd != -1) + { + xclose (self->irc_fd); + poller_fd_reset (&self->irc_event); + } + if (self->ssl) + SSL_free (self->ssl); + if (self->ssl_ctx) + SSL_CTX_free (self->ssl_ctx); + + poller_free (&self->poller); + free (self->readline_prompt); + + iconv_close (self->term_from_latin1); + iconv_close (self->term_from_utf8); + iconv_close (self->term_to_utf8); +} + +// --- Attributed output ------------------------------------------------------- + +static struct +{ + bool initialized; ///< Terminal is available + bool stdout_is_tty; ///< `stdout' is a terminal + bool stderr_is_tty; ///< `stderr' is a terminal + + char *color_set[8]; ///< Codes to set the foreground colour +} +g_terminal; + +static bool +init_terminal (void) +{ + int tty_fd = -1; + if ((g_terminal.stderr_is_tty = isatty (STDERR_FILENO))) + tty_fd = STDERR_FILENO; + if ((g_terminal.stdout_is_tty = isatty (STDOUT_FILENO))) + tty_fd = STDOUT_FILENO; + + int err; + if (tty_fd == -1 || setupterm (NULL, tty_fd, &err) == ERR) + return false; + + // Make sure all terminal features used by us are supported + if (!set_a_foreground || !enter_bold_mode || !exit_attribute_mode) + { + del_curterm (cur_term); + return false; + } + + for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set); i++) + g_terminal.color_set[i] = xstrdup (tparm (set_a_foreground, + i, 0, 0, 0, 0, 0, 0, 0, 0)); + + return g_terminal.initialized = true; +} + +static void +free_terminal (void) +{ + if (!g_terminal.initialized) + return; + + for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set); i++) + free (g_terminal.color_set[i]); + del_curterm (cur_term); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct app_readline_state +{ + char *saved_line; + int saved_point; +}; + +static void +app_readline_hide (struct app_readline_state *state) +{ + state->saved_point = rl_point; + state->saved_line = rl_copy_text (0, rl_end); + rl_set_prompt (""); + rl_replace_line ("", 0); + rl_redisplay (); +} + +static void +app_readline_restore (struct app_readline_state *state, const char *prompt) +{ + rl_set_prompt (prompt); + rl_replace_line (state->saved_line, 0); + rl_point = state->saved_point; + rl_redisplay (); + free (state->saved_line); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +typedef int (*terminal_printer_fn) (int); + +static int +putchar_stderr (int c) +{ + return fputc (c, stderr); +} + +static terminal_printer_fn +get_attribute_printer (FILE *stream) +{ + if (stream == stdout && g_terminal.stdout_is_tty) + return putchar; + if (stream == stderr && g_terminal.stderr_is_tty) + return putchar_stderr; + return NULL; +} + +static void +vprint_attributed (struct app_context *ctx, + FILE *stream, const char *attribute, const char *fmt, va_list ap) +{ + terminal_printer_fn printer = get_attribute_printer (stream); + if (!attribute) + printer = NULL; + + if (printer) + { + const char *value = str_map_find (&ctx->config, attribute); + tputs (value, 1, printer); + } + + vfprintf (stream, fmt, ap); + + if (printer) + { + const char *value = str_map_find (&ctx->config, ATTR_RESET); + tputs (value, 1, printer); + } +} + +static void +print_attributed (struct app_context *ctx, + FILE *stream, const char *attribute, const char *fmt, ...) +{ + va_list ap; + va_start (ap, fmt); + vprint_attributed (ctx, stream, attribute, fmt, ap); + va_end (ap); +} + +static void +log_message_attributed (void *user_data, const char *quote, const char *fmt, + va_list ap) +{ + FILE *stream = stderr; + + // GNU readline is a huge piece of total crap; it seems that we must do + // these incredible shenanigans in order to intersperse readline output + // with asynchronous status messages + struct app_readline_state state; + if (g_ctx->readline_prompt_shown) + app_readline_hide (&state); + + print_attributed (g_ctx, stream, user_data, "%s", quote); + vprint_attributed (g_ctx, stream, user_data, fmt, ap); + fputs ("\n", stream); + + if (g_ctx->readline_prompt_shown) + app_readline_restore (&state, g_ctx->readline_prompt); +} + +static void +init_colors (struct app_context *ctx) +{ + // Use escape sequences from terminfo if possible, and SGR as a fallback + if (init_terminal ()) + { + const char *attrs[][2] = + { + { ATTR_PROMPT, enter_bold_mode }, + { ATTR_RESET, exit_attribute_mode }, + { ATTR_WARNING, g_terminal.color_set[3] }, + { ATTR_ERROR, g_terminal.color_set[1] }, + }; + for (size_t i = 0; i < N_ELEMENTS (attrs); i++) + str_map_set (&ctx->config, attrs[i][0], xstrdup (attrs[i][1])); + } + else + { + const char *attrs[][2] = + { + { ATTR_PROMPT, "\x1b[1m" }, + { ATTR_RESET, "\x1b[0m" }, + { ATTR_WARNING, "\x1b[33m" }, + { ATTR_ERROR, "\x1b[31m" }, + }; + for (size_t i = 0; i < N_ELEMENTS (attrs); i++) + str_map_set (&ctx->config, attrs[i][0], xstrdup (attrs[i][1])); + } + + switch (ctx->color_mode) + { + case COLOR_ALWAYS: + g_terminal.stdout_is_tty = true; + g_terminal.stderr_is_tty = true; + break; + case COLOR_AUTO: + if (!g_terminal.initialized) + { + case COLOR_NEVER: + g_terminal.stdout_is_tty = false; + g_terminal.stderr_is_tty = false; + } + } + + g_log_message_real = log_message_attributed; +} + +// --- Signals ----------------------------------------------------------------- + +static int g_signal_pipe[2]; ///< A pipe used to signal... signals + +/// Program termination has been requested by a signal +static volatile sig_atomic_t g_termination_requested; +/// The window has changed in size +static volatile sig_atomic_t g_winch_received; + +static void +sigterm_handler (int signum) +{ + (void) signum; + + g_termination_requested = true; + + int original_errno = errno; + if (write (g_signal_pipe[1], "t", 1) == -1) + soft_assert (errno == EAGAIN); + errno = original_errno; +} + +static void +sigwinch_handler (int signum) +{ + (void) signum; + + g_winch_received = true; + + int original_errno = errno; + if (write (g_signal_pipe[1], "w", 1) == -1) + soft_assert (errno == EAGAIN); + errno = original_errno; +} + +static void +setup_signal_handlers (void) +{ + if (pipe (g_signal_pipe) == -1) + exit_fatal ("%s: %s", "pipe", strerror (errno)); + + set_cloexec (g_signal_pipe[0]); + set_cloexec (g_signal_pipe[1]); + + // So that the pipe cannot overflow; it would make write() block within + // the signal handler, which is something we really don't want to happen. + // The same holds true for read(). + set_blocking (g_signal_pipe[0], false); + set_blocking (g_signal_pipe[1], false); + + signal (SIGPIPE, SIG_IGN); + + struct sigaction sa; + sa.sa_flags = SA_RESTART; + sa.sa_handler = sigwinch_handler; + sigemptyset (&sa.sa_mask); + + if (sigaction (SIGWINCH, &sa, NULL) == -1) + exit_fatal ("sigaction: %s", strerror (errno)); + + sa.sa_handler = sigterm_handler; + if (sigaction (SIGINT, &sa, NULL) == -1 + || sigaction (SIGTERM, &sa, NULL) == -1) + exit_fatal ("sigaction: %s", strerror (errno)); +} + +// --- Buffers ----------------------------------------------------------------- + +static void +send_to_buffer (struct app_context *ctx, struct buffer *buffer, + enum buffer_line_type type, int flags, + const char *origin, const char *format, ...); + +static void +prepare_buffers (struct app_context *ctx) +{ + struct buffer *global = ctx->global_buffer = buffer_new (); + struct buffer *server = ctx->server_buffer = buffer_new (); + + global->type = BUFFER_GLOBAL; + global->name = xstrdup (PROGRAM_NAME); + + server->type = BUFFER_SERVER; + server->name = xstrdup (str_map_find (&ctx->config, "irc_host")); + + LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, global); + LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, server); +} + +// --- Supporting code --------------------------------------------------------- + +static void +irc_shutdown (struct app_context *ctx) +{ + // TODO: set a timer after which we cut the connection? + // Generally non-critical + if (ctx->ssl) + soft_assert (SSL_shutdown (ctx->ssl) != -1); + else + soft_assert (shutdown (ctx->irc_fd, SHUT_WR) == 0); +} + +static void +try_finish_quit (struct app_context *ctx) +{ + if (ctx->quitting && ctx->irc_fd == -1) + ctx->polling = false; +} + +static void +initiate_quit (struct app_context *ctx) +{ + print_status ("shutting down"); + if (ctx->irc_fd != -1) + irc_shutdown (ctx); + + ctx->quitting = true; + try_finish_quit (ctx); +} + +static bool irc_send (struct app_context *ctx, + const char *format, ...) ATTRIBUTE_PRINTF (2, 3); + +static bool +irc_send (struct app_context *ctx, const char *format, ...) +{ + va_list ap; + + if (g_debug_mode) + { + struct app_readline_state state; + if (ctx->readline_prompt_shown) + app_readline_hide (&state); + + fputs ("[IRC] <== \"", stderr); + va_start (ap, format); + vfprintf (stderr, format, ap); + va_end (ap); + fputs ("\"\n", stderr); + + if (ctx->readline_prompt_shown) + app_readline_restore (&state, ctx->readline_prompt); + } + + if (!soft_assert (ctx->irc_fd != -1)) + return false; + + va_start (ap, format); + struct str str; + str_init (&str); + str_append_vprintf (&str, format, ap); + str_append (&str, "\r\n"); + va_end (ap); + + bool result = true; + if (ctx->ssl) + { + // TODO: call SSL_get_error() to detect if a clean shutdown has occured + if (SSL_write (ctx->ssl, str.str, str.len) != (int) str.len) + { + print_debug ("%s: %s: %s", __func__, "SSL_write", + ERR_error_string (ERR_get_error (), NULL)); + result = false; + } + } + else if (write (ctx->irc_fd, str.str, str.len) != (ssize_t) str.len) + { + print_debug ("%s: %s: %s", __func__, "write", strerror (errno)); + result = false; + } + + str_free (&str); + return result; +} + +static bool +irc_get_boolean_from_config + (struct app_context *ctx, const char *name, bool *value, struct error **e) +{ + const char *str = str_map_find (&ctx->config, name); + hard_assert (str != NULL); + + if (set_boolean_if_valid (value, str)) + return true; + + error_set (e, "invalid configuration value for `%s'", name); + return false; +} + +static bool +irc_initialize_ssl_ctx (struct app_context *ctx, struct error **e) +{ + // XXX: maybe we should call SSL_CTX_set_options() for some workarounds + + bool verify; + if (!irc_get_boolean_from_config (ctx, "ssl_verify", &verify, e)) + return false; + + if (!verify) + SSL_CTX_set_verify (ctx->ssl_ctx, SSL_VERIFY_NONE, NULL); + + const char *ca_file = str_map_find (&ctx->config, "ca_file"); + const char *ca_path = str_map_find (&ctx->config, "ca_path"); + + struct error *error = NULL; + if (ca_file || ca_path) + { + if (SSL_CTX_load_verify_locations (ctx->ssl_ctx, ca_file, ca_path)) + return true; + + error_set (&error, "%s: %s", + "failed to set locations for the CA certificate bundle", + ERR_reason_error_string (ERR_get_error ())); + goto ca_error; + } + + if (!SSL_CTX_set_default_verify_paths (ctx->ssl_ctx)) + { + error_set (&error, "%s: %s", + "couldn't load the default CA certificate bundle", + ERR_reason_error_string (ERR_get_error ())); + goto ca_error; + } + return true; + +ca_error: + if (verify) + { + error_propagate (e, error); + return false; + } + + // Only inform the user if we're not actually verifying + print_warning ("%s", error->message); + error_free (error); + return true; +} + +static bool +irc_initialize_ssl (struct app_context *ctx, struct error **e) +{ + const char *error_info = NULL; + ctx->ssl_ctx = SSL_CTX_new (SSLv23_client_method ()); + if (!ctx->ssl_ctx) + goto error_ssl_1; + if (!irc_initialize_ssl_ctx (ctx, e)) + goto error_ssl_2; + + ctx->ssl = SSL_new (ctx->ssl_ctx); + if (!ctx->ssl) + goto error_ssl_2; + + const char *ssl_cert = str_map_find (&ctx->config, "ssl_cert"); + if (ssl_cert) + { + char *path = resolve_config_filename (ssl_cert); + if (!path) + print_error ("%s: %s", "cannot open file", ssl_cert); + // XXX: perhaps we should read the file ourselves for better messages + else if (!SSL_use_certificate_file (ctx->ssl, path, SSL_FILETYPE_PEM) + || !SSL_use_PrivateKey_file (ctx->ssl, path, SSL_FILETYPE_PEM)) + print_error ("%s: %s", "setting the SSL client certificate failed", + ERR_error_string (ERR_get_error (), NULL)); + free (path); + } + + SSL_set_connect_state (ctx->ssl); + if (!SSL_set_fd (ctx->ssl, ctx->irc_fd)) + goto error_ssl_3; + // Avoid SSL_write() returning SSL_ERROR_WANT_READ + SSL_set_mode (ctx->ssl, SSL_MODE_AUTO_RETRY); + + switch (xssl_get_error (ctx->ssl, SSL_connect (ctx->ssl), &error_info)) + { + case SSL_ERROR_NONE: + return true; + case SSL_ERROR_ZERO_RETURN: + error_info = "server closed the connection"; + default: + break; + } + +error_ssl_3: + SSL_free (ctx->ssl); + ctx->ssl = NULL; +error_ssl_2: + SSL_CTX_free (ctx->ssl_ctx); + ctx->ssl_ctx = NULL; +error_ssl_1: + // XXX: these error strings are really nasty; also there could be + // multiple errors on the OpenSSL stack. + if (!error_info) + error_info = ERR_error_string (ERR_get_error (), NULL); + error_set (e, "%s: %s", "could not initialize SSL", error_info); + return false; +} + +static bool +irc_establish_connection (struct app_context *ctx, + const char *host, const char *port, struct error **e) +{ + struct addrinfo gai_hints, *gai_result, *gai_iter; + memset (&gai_hints, 0, sizeof gai_hints); + gai_hints.ai_socktype = SOCK_STREAM; + + int err = getaddrinfo (host, port, &gai_hints, &gai_result); + if (err) + { + error_set (e, "%s: %s: %s", + "connection failed", "getaddrinfo", gai_strerror (err)); + return false; + } + + int sockfd; + for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next) + { + sockfd = socket (gai_iter->ai_family, + gai_iter->ai_socktype, gai_iter->ai_protocol); + if (sockfd == -1) + continue; + set_cloexec (sockfd); + + int yes = 1; + soft_assert (setsockopt (sockfd, SOL_SOCKET, SO_KEEPALIVE, + &yes, sizeof yes) != -1); + + const char *real_host = host; + + // Let's try to resolve the address back into a real hostname; + // we don't really need this, so we can let it quietly fail + char buf[NI_MAXHOST]; + err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen, + buf, sizeof buf, NULL, 0, NI_NUMERICHOST); + if (err) + print_debug ("%s: %s", "getnameinfo", gai_strerror (err)); + else + real_host = buf; + + // XXX: we shouldn't mix these statuses with `struct error'; choose 1! + char *address = format_host_port_pair (real_host, port); + print_status ("connecting to %s...", address); + free (address); + + if (!connect (sockfd, gai_iter->ai_addr, gai_iter->ai_addrlen)) + break; + + xclose (sockfd); + } + + freeaddrinfo (gai_result); + + if (!gai_iter) + { + error_set (e, "connection failed"); + return false; + } + + ctx->irc_fd = sockfd; + return true; +} + +static void +refresh_prompt (struct app_context *ctx) +{ + bool have_attributes = !!get_attribute_printer (stdout); + + struct str prompt; + str_init (&prompt); + + if (!ctx->irc_ready) + str_append (&prompt, "(disconnected)"); + else + { + str_append_c (&prompt, '['); + // TODO: go through all buffers and prepend the active ones, + // such as "(1,4) " + + // TODO: name of the current buffer + // TODO: the channel user mode + str_append (&prompt, str_map_find (&ctx->config, "nickname")); + // TODO: user mode in parenthesis + str_append_c (&prompt, ']'); + } + str_append (&prompt, " "); + + // After building the new prompt, replace the old one + free (ctx->readline_prompt); + + if (!have_attributes) + ctx->readline_prompt = xstrdup (prompt.str); + else + { + // XXX: to be completely correct, we should use tputs, but we cannot + const char *prompt_attrs = str_map_find (&ctx->config, ATTR_PROMPT); + const char *reset_attrs = str_map_find (&ctx->config, ATTR_RESET); + ctx->readline_prompt = xstrdup_printf ("%c%s%c%s%c%s%c", + RL_PROMPT_START_IGNORE, prompt_attrs, RL_PROMPT_END_IGNORE, + prompt.str, + RL_PROMPT_START_IGNORE, reset_attrs, RL_PROMPT_END_IGNORE); + } + str_free (&prompt); + + // FIXME: when the program hasn't displayed the prompt yet, is this okay? + rl_redisplay (); +} + +// --- Input handling ---------------------------------------------------------- + +static void +irc_process_message (const struct irc_message *msg, + const char *raw, void *user_data) +{ + struct app_context *ctx = user_data; + + if (g_debug_mode) + { + struct app_readline_state state; + if (ctx->readline_prompt_shown) + app_readline_hide (&state); + + // TODO: ensure proper encoding + fprintf (stderr, "[%s] ==> \"%s\"\n", "IRC", raw); + + if (ctx->readline_prompt_shown) + app_readline_restore (&state, ctx->readline_prompt); + } + + bool show_to_user = true; + if (!strcasecmp (msg->command, "PING")) + { + show_to_user = false; + if (msg->params.len) + irc_send (ctx, "PONG :%s", msg->params.vector[0]); + else + irc_send (ctx, "PONG"); + } + else if (!ctx->irc_ready && (!strcasecmp (msg->command, "MODE") + || !strcasecmp (msg->command, "376") // RPL_ENDOFMOTD + || !strcasecmp (msg->command, "422"))) // ERR_NOMOTD + { + print_status ("successfully connected"); + ctx->irc_ready = true; + + const char *autojoin = str_map_find (&ctx->config, "autojoin"); + if (autojoin) + irc_send (ctx, "JOIN :%s", autojoin); + } + else + { + // TODO: whatever processing we need + } + + // This is going to be a lot more complicated + if (show_to_user) + // TODO: ensure proper encoding + print_status ("%s", raw); +} + +// TODO: load and preprocess this table so that shortcuts are accepted +struct command_handler +{ + char *name; + void (*handler) (struct app_context *ctx, const char *arguments); +} +g_handlers[] = +{ + { "buffer", NULL }, + { "help", NULL }, + + { "msg", NULL }, + { "query", NULL }, + { "notice", NULL }, + { "ctcp", NULL }, + { "me", NULL }, + + { "join", NULL }, + { "part", NULL }, + { "cycle", NULL }, + + { "mode", NULL }, + { "topic", NULL }, + { "kick", NULL }, + { "kickban", NULL }, + { "ban", NULL }, + { "invite", NULL }, + + { "list", NULL }, + { "names", NULL }, + { "who", NULL }, + { "whois", NULL }, + + { "motd", NULL }, + { "away", NULL }, + { "quote", NULL }, + { "quit", NULL }, +}; + +static void +process_internal_command (struct app_context *ctx, const char *command) +{ + // TODO: resolve commands from a map + // TODO: if it's a number, switch to the given buffer + + if (!strcmp (command, "quit")) + initiate_quit (ctx); +} + +static void +process_input (struct app_context *ctx, char *user_input) +{ + char *input; + size_t len; + + if (!(input = iconv_xstrdup (ctx->term_to_utf8, user_input, -1, &len))) + { + print_error ("character conversion failed for `%s'", "user input"); + goto fail; + } + + if (*input == '/') + process_internal_command (ctx, input + 1); + else + { + // TODO: send a message to the current buffer + } + +fail: + free (input); +} + +static int +on_readline_previous_buffer (int count, int key) +{ + (void) key; + + // TODO: switch to the previous buffer + return 0; +} + +static int +on_readline_next_buffer (int count, int key) +{ + (void) key; + + // TODO: switch to the next buffer + return 0; +} + +// --- Supporting code (continued) --------------------------------------------- + +enum irc_read_result +{ + IRC_READ_OK, ///< Some data were read successfully + IRC_READ_EOF, ///< The server has closed connection + IRC_READ_AGAIN, ///< No more data at the moment + IRC_READ_ERROR ///< General connection failure +}; + +static enum irc_read_result +irc_fill_read_buffer_ssl (struct app_context *ctx, struct str *buf) +{ + int n_read; +start: + n_read = SSL_read (ctx->ssl, buf->str + buf->len, + buf->alloc - buf->len - 1 /* null byte */); + + const char *error_info = NULL; + switch (xssl_get_error (ctx->ssl, n_read, &error_info)) + { + case SSL_ERROR_NONE: + buf->str[buf->len += n_read] = '\0'; + return IRC_READ_OK; + case SSL_ERROR_ZERO_RETURN: + return IRC_READ_EOF; + case SSL_ERROR_WANT_READ: + return IRC_READ_AGAIN; + case SSL_ERROR_WANT_WRITE: + { + // Let it finish the handshake as we don't poll for writability; + // any errors are to be collected by SSL_read() in the next iteration + struct pollfd pfd = { .fd = ctx->irc_fd, .events = POLLOUT }; + soft_assert (poll (&pfd, 1, 0) > 0); + goto start; + } + case XSSL_ERROR_TRY_AGAIN: + goto start; + default: + print_debug ("%s: %s: %s", __func__, "SSL_read", error_info); + return IRC_READ_ERROR; + } +} + +static enum irc_read_result +irc_fill_read_buffer (struct app_context *ctx, struct str *buf) +{ + ssize_t n_read; +start: + n_read = recv (ctx->irc_fd, buf->str + buf->len, + buf->alloc - buf->len - 1 /* null byte */, 0); + + if (n_read > 0) + { + buf->str[buf->len += n_read] = '\0'; + return IRC_READ_OK; + } + if (n_read == 0) + return IRC_READ_EOF; + + if (errno == EAGAIN) + return IRC_READ_AGAIN; + if (errno == EINTR) + goto start; + + print_debug ("%s: %s: %s", __func__, "recv", strerror (errno)); + return IRC_READ_ERROR; +} + +static bool irc_connect (struct app_context *, struct error **); +static void irc_queue_reconnect (struct app_context *); + +static void +irc_cancel_timers (struct app_context *ctx) +{ + poller_timer_reset (&ctx->timeout_tmr); + poller_timer_reset (&ctx->ping_tmr); + poller_timer_reset (&ctx->reconnect_tmr); +} + +static void +on_irc_reconnect_timeout (void *user_data) +{ + struct app_context *ctx = user_data; + + struct error *e = NULL; + if (irc_connect (ctx, &e)) + return; + + print_error ("%s", e->message); + error_free (e); + irc_queue_reconnect (ctx); +} + +static void +irc_queue_reconnect (struct app_context *ctx) +{ + hard_assert (ctx->irc_fd == -1); + print_status ("trying to reconnect in %ld seconds...", + ctx->reconnect_delay); + poller_timer_set (&ctx->reconnect_tmr, ctx->reconnect_delay * 1000); +} + +static void +on_irc_disconnected (struct app_context *ctx) +{ + // Get rid of the dead socket and related things + if (ctx->ssl) + { + SSL_free (ctx->ssl); + ctx->ssl = NULL; + SSL_CTX_free (ctx->ssl_ctx); + ctx->ssl_ctx = NULL; + } + + xclose (ctx->irc_fd); + ctx->irc_fd = -1; + ctx->irc_ready = false; + + ctx->irc_event.closed = true; + poller_fd_reset (&ctx->irc_event); + + // All of our timers have lost their meaning now + irc_cancel_timers (ctx); + + if (ctx->quitting) + try_finish_quit (ctx); + else if (!ctx->reconnect) + initiate_quit (ctx); + else + irc_queue_reconnect (ctx); +} + +static void +on_irc_ping_timeout (void *user_data) +{ + struct app_context *ctx = user_data; + print_error ("connection timeout"); + on_irc_disconnected (ctx); +} + +static void +on_irc_timeout (void *user_data) +{ + // Provoke a response from the server + struct app_context *ctx = user_data; + irc_send (ctx, "PING :%s", + (char *) str_map_find (&ctx->config, "nickname")); +} + +static void +irc_reset_connection_timeouts (struct app_context *ctx) +{ + irc_cancel_timers (ctx); + poller_timer_set (&ctx->timeout_tmr, 3 * 60 * 1000); + poller_timer_set (&ctx->ping_tmr, (3 * 60 + 30) * 1000); +} + +static void +on_irc_readable (const struct pollfd *fd, struct app_context *ctx) +{ + if (fd->revents & ~(POLLIN | POLLHUP | POLLERR)) + print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents); + + (void) set_blocking (ctx->irc_fd, false); + + struct str *buf = &ctx->read_buffer; + enum irc_read_result (*fill_buffer)(struct app_context *, struct str *) + = ctx->ssl + ? irc_fill_read_buffer_ssl + : irc_fill_read_buffer; + bool disconnected = false; + while (true) + { + str_ensure_space (buf, 512); + switch (fill_buffer (ctx, buf)) + { + case IRC_READ_AGAIN: + goto end; + case IRC_READ_ERROR: + print_error ("reading from the IRC server failed"); + disconnected = true; + goto end; + case IRC_READ_EOF: + print_status ("the IRC server closed the connection"); + disconnected = true; + goto end; + case IRC_READ_OK: + break; + } + + if (buf->len >= (1 << 20)) + { + print_error ("the IRC server seems to spew out data frantically"); + irc_shutdown (ctx); + goto end; + } + } +end: + (void) set_blocking (ctx->irc_fd, true); + irc_process_buffer (buf, irc_process_message, ctx); + + if (disconnected) + on_irc_disconnected (ctx); + else + irc_reset_connection_timeouts (ctx); +} + +static bool +irc_connect (struct app_context *ctx, struct error **e) +{ + const char *irc_host = str_map_find (&ctx->config, "irc_host"); + const char *irc_port = str_map_find (&ctx->config, "irc_port"); + + const char *socks_host = str_map_find (&ctx->config, "socks_host"); + const char *socks_port = str_map_find (&ctx->config, "socks_port"); + const char *socks_username = str_map_find (&ctx->config, "socks_username"); + const char *socks_password = str_map_find (&ctx->config, "socks_password"); + + const char *nickname = str_map_find (&ctx->config, "nickname"); + const char *username = str_map_find (&ctx->config, "username"); + const char *realname = str_map_find (&ctx->config, "realname"); + + // We have a default value for these + hard_assert (irc_port && socks_port); + + // These are filled automatically if needed + hard_assert (nickname && username && realname); + + // TODO: again, get rid of `struct error' in here. The question is: how + // do we tell our caller that he should not try to reconnect? + if (!irc_host) + { + error_set (e, "no hostname specified in configuration"); + return false; + } + + bool use_ssl; + if (!irc_get_boolean_from_config (ctx, "ssl", &use_ssl, e)) + return false; + + if (socks_host) + { + char *address = format_host_port_pair (irc_host, irc_port); + char *socks_address = format_host_port_pair (socks_host, socks_port); + print_status ("connecting to %s via %s...", address, socks_address); + free (socks_address); + free (address); + + struct error *error = NULL; + int fd = socks_connect (socks_host, socks_port, irc_host, irc_port, + socks_username, socks_password, &error); + if (fd == -1) + { + error_set (e, "%s: %s", "SOCKS connection failed", error->message); + error_free (error); + return false; + } + ctx->irc_fd = fd; + } + else if (!irc_establish_connection (ctx, irc_host, irc_port, e)) + return false; + + if (use_ssl && !irc_initialize_ssl (ctx, e)) + { + xclose (ctx->irc_fd); + ctx->irc_fd = -1; + return false; + } + print_status ("connection established"); + + poller_fd_init (&ctx->irc_event, &ctx->poller, ctx->irc_fd); + ctx->irc_event.dispatcher = (poller_fd_fn) on_irc_readable; + ctx->irc_event.user_data = ctx; + + poller_fd_set (&ctx->irc_event, POLLIN); + irc_reset_connection_timeouts (ctx); + + irc_send (ctx, "NICK %s", nickname); + irc_send (ctx, "USER %s 8 * :%s", username, realname); + return true; +} + +// --- I/O event handlers ------------------------------------------------------ + +static void +on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx) +{ + char dummy; + (void) read (fd->fd, &dummy, 1); + + if (g_termination_requested && !ctx->quitting) + { + // There may be a timer set to reconnect to the server + irc_cancel_timers (ctx); + + if (ctx->irc_fd != -1) + irc_send (ctx, "QUIT :Terminated by signal"); + initiate_quit (ctx); + } + + if (g_winch_received) + // This fucks up big time on terminals with automatic wrapping such as + // rxvt-unicode or newer VTE when the current line overflows, however we + // can't do much about that + rl_resize_terminal (); +} + +static void +on_tty_readable (const struct pollfd *fd, struct app_context *ctx) +{ + (void) ctx; + + if (fd->revents & ~(POLLIN | POLLHUP | POLLERR)) + print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents); + + rl_callback_read_char (); +} + +static void +on_readline_input (char *line) +{ + // Otherwise the prompt is shown at all times + g_ctx->readline_prompt_shown = false; + + if (line) + { + if (*line) + add_history (line); + + process_input (g_ctx, line); + free (line); + } + + g_ctx->readline_prompt_shown = true; +} + +// --- Configuration loading --------------------------------------------------- + +static bool +read_hexa_escape (const char **cursor, struct str *output) +{ + int i; + char c, code = 0; + + for (i = 0; i < 2; i++) + { + c = tolower (*(*cursor)); + if (c >= '0' && c <= '9') + code = (code << 4) | (c - '0'); + else if (c >= 'a' && c <= 'f') + code = (code << 4) | (c - 'a' + 10); + else + break; + + (*cursor)++; + } + + if (!i) + return false; + + str_append_c (output, code); + return true; +} + +static bool +read_octal_escape (const char **cursor, struct str *output) +{ + int i; + char c, code = 0; + + for (i = 0; i < 3; i++) + { + c = *(*cursor); + if (c < '0' || c > '7') + break; + + code = (code << 3) | (c - '0'); + (*cursor)++; + } + + if (!i) + return false; + + str_append_c (output, code); + return true; +} + +static bool +read_string_escape_sequence (const char **cursor, + struct str *output, struct error **e) +{ + int c; + switch ((c = *(*cursor)++)) + { + case '?': str_append_c (output, '?'); break; + case '"': str_append_c (output, '"'); break; + case '\\': str_append_c (output, '\\'); break; + case 'a': str_append_c (output, '\a'); break; + case 'b': str_append_c (output, '\b'); break; + case 'f': str_append_c (output, '\f'); break; + case 'n': str_append_c (output, '\n'); break; + case 'r': str_append_c (output, '\r'); break; + case 't': str_append_c (output, '\t'); break; + case 'v': str_append_c (output, '\v'); break; + + case 'e': + case 'E': + str_append_c (output, '\x1b'); + break; + + case 'x': + case 'X': + if (!read_hexa_escape (cursor, output)) + FAIL ("invalid hexadecimal escape"); + break; + + case '\0': + FAIL ("premature end of escape sequence"); + + default: + (*cursor)--; + if (!read_octal_escape (cursor, output)) + FAIL ("unknown escape sequence"); + } + return true; +} + +static bool +unescape_string (const char *s, struct str *output, struct error **e) +{ + int c; + while ((c = *s++)) + { + if (c != '\\') + str_append_c (output, c); + else if (!read_string_escape_sequence (&s, output, e)) + return false; + } + return true; +} + +static bool +autofill_user_info (struct app_context *ctx, struct error **e) +{ + const char *nickname = str_map_find (&ctx->config, "nickname"); + const char *username = str_map_find (&ctx->config, "username"); + const char *realname = str_map_find (&ctx->config, "realname"); + + if (nickname && username && realname) + return true; + + // TODO: read POSIX user info and fill the configuration if needed + struct passwd *pwd = getpwuid (geteuid ()); + if (!pwd) + FAIL ("cannot retrieve user information: %s", strerror (errno)); + + if (!nickname) + str_map_set (&ctx->config, "nickname", xstrdup (pwd->pw_name)); + if (!username) + str_map_set (&ctx->config, "username", xstrdup (pwd->pw_name)); + + // Not all systems have the GECOS field but the vast majority does + if (!realname) + { + char *gecos = pwd->pw_gecos; + + // The first comma, if any, ends the user's real name + char *comma = strchr (gecos, ','); + if (comma) + *comma = '\0'; + + str_map_set (&ctx->config, "username", xstrdup (gecos)); + } + + return true; +} + +static bool +load_config (struct app_context *ctx, struct error **e) +{ + // TODO: employ a better configuration file format, so that we don't have + // to do this convoluted post-processing anymore. + + struct str_map map; + str_map_init (&map); + map.free = free; + + if (!read_config_file (&map, e)) + return false; + + struct str_map_iter iter; + str_map_iter_init (&iter, &map); + while (str_map_iter_next (&iter)) + { + struct error *e = NULL; + struct str value; + str_init (&value); + if (!unescape_string (iter.link->data, &value, &e)) + { + print_error ("error reading configuration: %s: %s", + iter.link->key, e->message); + error_free (e); + exit (EXIT_FAILURE); + } + + str_map_set (&ctx->config, iter.link->key, str_steal (&value)); + } + + if (!autofill_user_info (ctx, e)) + return false; + + if (!irc_get_boolean_from_config (ctx, "reconnect", &ctx->reconnect, e)) + return false; + + const char *delay_str = str_map_find (&ctx->config, "reconnect_delay"); + hard_assert (delay_str != NULL); // We have a default value for this + if (!xstrtoul (&ctx->reconnect_delay, delay_str, 10)) + { + error_set (e, "invalid configuration value for `%s'", + "reconnect_delay"); + return false; + } + return true; +} + +// --- Main program ------------------------------------------------------------ + +static void +init_poller_events (struct app_context *ctx) +{ + poller_timer_init (&ctx->timeout_tmr, &ctx->poller); + ctx->timeout_tmr.dispatcher = on_irc_timeout; + ctx->timeout_tmr.user_data = ctx; + + poller_timer_init (&ctx->ping_tmr, &ctx->poller); + ctx->ping_tmr.dispatcher = on_irc_ping_timeout; + ctx->ping_tmr.user_data = ctx; + + poller_timer_init (&ctx->reconnect_tmr, &ctx->poller); + ctx->reconnect_tmr.dispatcher = on_irc_reconnect_timeout; + ctx->reconnect_tmr.user_data = ctx; + + poller_fd_init (&ctx->signal_event, &ctx->poller, g_signal_pipe[0]); + ctx->signal_event.dispatcher = (poller_fd_fn) on_signal_pipe_readable; + ctx->signal_event.user_data = &ctx; + poller_fd_set (&ctx->signal_event, POLLIN); + + poller_fd_init (&ctx->tty_event, &ctx->poller, STDIN_FILENO); + ctx->tty_event.dispatcher = (poller_fd_fn) on_tty_readable; + ctx->tty_event.user_data = &ctx; + poller_fd_set (&ctx->tty_event, POLLIN); +} + +int +main (int argc, char *argv[]) +{ + static const struct opt opts[] = + { + { 'd', "debug", NULL, 0, "run in debug mode" }, + { 'h', "help", NULL, 0, "display this help and exit" }, + { 'V', "version", NULL, 0, "output version information and exit" }, + { 'w', "write-default-cfg", "FILENAME", + OPT_OPTIONAL_ARG | OPT_LONG_ONLY, + "write a default configuration file and exit" }, + { 0, NULL, NULL, 0, NULL } + }; + + struct opt_handler oh; + opt_handler_init (&oh, argc, argv, opts, NULL, "Experimental IRC client."); + + int c; + while ((c = opt_handler_get (&oh)) != -1) + switch (c) + { + case 'd': + g_debug_mode = true; + break; + case 'h': + opt_handler_usage (&oh, stdout); + exit (EXIT_SUCCESS); + case 'V': + printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); + exit (EXIT_SUCCESS); + case 'w': + call_write_default_config (optarg, g_config_table); + exit (EXIT_SUCCESS); + default: + print_error ("wrong options"); + opt_handler_usage (&oh, stderr); + exit (EXIT_FAILURE); + } + + opt_handler_free (&oh); + + struct app_context ctx; + print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting"); + app_context_init (&ctx); + g_ctx = &ctx; + + // We only need to convert to and from the terminal encoding + setlocale (LC_CTYPE, ""); + + SSL_library_init (); + atexit (EVP_cleanup); + SSL_load_error_strings (); + atexit (ERR_free_strings); + + using_history (); + stifle_history (HISTORY_LIMIT); + + init_colors (&ctx); + init_poller_events (&ctx); + + struct error *e = NULL; + if (!load_config (&ctx, &e) + || !irc_connect (&ctx, &e)) + { + print_error ("%s", e->message); + error_free (e); + exit (EXIT_FAILURE); + } + + setup_signal_handlers (); + prepare_buffers (&ctx); + refresh_prompt (&ctx); + + // TODO: maybe use rl_make_bare_keymap() and start from there + + // XXX: Since readline() installs a set of default key bindings the first + // time it is called, there is always the danger that a custom binding + // installed before the first call to readline() will be overridden. + // An alternate mechanism is to install custom key bindings in an + // initialization function assigned to the rl_startup_hook variable. + rl_add_defun ("previous-buffer", on_readline_previous_buffer, -1); + rl_add_defun ("next-buffer", on_readline_next_buffer, -1); + + // TODO: redefine M-0 through M-9 to switch buffers + rl_bind_keyseq ("C-p", rl_named_function ("previous-buffer")); + rl_bind_keyseq ("C-n", rl_named_function ("next-buffer")); + rl_bind_keyseq ("M-p", rl_named_function ("previous-history")); + rl_bind_keyseq ("M-n", rl_named_function ("next-history")); + + rl_catch_sigwinch = false; + ctx.readline_prompt_shown = true; + rl_callback_handler_install (ctx.readline_prompt, on_readline_input); + + ctx.polling = true; + while (ctx.polling) + poller_run (&ctx.poller); + + if (ctx.readline_prompt_shown) + rl_callback_handler_remove (); + putchar ('\n'); + + app_context_free (&ctx); + free_terminal (); + return EXIT_SUCCESS; +}