summaryrefslogtreecommitdiff
path: root/degesch.c
diff options
context:
space:
mode:
Diffstat (limited to 'degesch.c')
-rw-r--r--degesch.c1707
1 files changed, 1707 insertions, 0 deletions
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 <p.janouch@gmail.com>
+ * 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 <langinfo.h>
+#include <locale.h>
+#include <pwd.h>
+
+#include <curses.h>
+#include <term.h>
+#include <readline/readline.h>
+#include <readline/history.h>
+
+// --- 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;
+}