xK/degesch.c

2316 lines
61 KiB
C

/*
* 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>
// Literally cancer
#undef lines
#undef columns
#include <readline/readline.h>
#include <readline/history.h>
// --- Configuration (application-specific) ------------------------------------
// TODO: reject all junk present in the configuration; there can be newlines
static struct config_item g_config_table[] =
{
{ "nickname", NULL, "IRC nickname" },
{ "username", NULL, "IRC user name" },
{ "realname", NULL, "IRC real name/e-mail" },
{ "irc_host", NULL, "Address of the IRC server" },
{ "irc_port", "6667", "Port of the IRC server" },
{ "ssl", "off", "Whether to use SSL" },
{ "ssl_cert", NULL, "Client SSL certificate (PEM)" },
{ "ssl_verify", "on", "Whether to verify certificates" },
{ "ssl_ca_file", NULL, "OpenSSL CA bundle file" },
{ "ssl_ca_path", NULL, "OpenSSL CA bundle path" },
{ "autojoin", NULL, "Channels to join on start" },
{ "reconnect", "on", "Whether to reconnect on error" },
{ "reconnect_delay", "5", "Time between reconnecting" },
{ "socks_host", NULL, "Address of a SOCKS 4a/5 proxy" },
{ "socks_port", "1080", "SOCKS port number" },
{ "socks_username", NULL, "SOCKS auth. username" },
{ "socks_password", NULL, "SOCKS auth. password" },
{ "isolate_buffers", "off", "Isolate global/server buffers" },
{ ATTR_PROMPT, NULL, "Terminal attributes for the prompt" },
{ ATTR_RESET, NULL, "String to reset terminal attributes" },
{ ATTR_WARNING, NULL, "Terminal attributes for warnings" },
{ ATTR_ERROR, NULL, "Terminal attributes for errors" },
{ NULL, NULL, NULL }
};
// --- Application data --------------------------------------------------------
// All text stored in our data structures is encoded in UTF-8.
// Or at least should be.
/// Shorthand to set an error and return failure from the function
#define FAIL(...) \
BLOCK_START \
error_set (e, __VA_ARGS__); \
return false; \
BLOCK_END
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
enum buffer_line_flags
{
BUFFER_LINE_HIGHLIGHT = 1 << 0 ///< The user was highlighted by this
};
enum buffer_line_type
{
BUFFER_LINE_PRIVMSG, ///< PRIVMSG
BUFFER_LINE_ACTION, ///< PRIVMSG ACTION
BUFFER_LINE_NOTICE, ///< NOTICE
BUFFER_LINE_JOIN, ///< JOIN
BUFFER_LINE_PART, ///< PART
BUFFER_LINE_KICK, ///< KICK
BUFFER_LINE_QUIT, ///< QUIT
BUFFER_LINE_STATUS, ///< Whatever status messages
BUFFER_LINE_ERROR ///< Whatever error messages
};
struct buffer_line
{
LIST_HEADER (struct buffer_line)
// We use the "type" and "flags" mostly just as formatting hints
enum buffer_line_type type; ///< Type of the event
int flags; ///< Flags
time_t when; ///< Time of the event
char *who; ///< Name of the origin or NULL (user)
char *object; ///< Text of message, object of action
char *reason; ///< Reason for PART, KICK, QUIT
};
struct buffer_line *
buffer_line_new (void)
{
struct buffer_line *self = xcalloc (1, sizeof *self);
return self;
}
static void
buffer_line_destroy (struct buffer_line *self)
{
free (self->who);
free (self->object);
free (self->reason);
free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct nick_info
{
char *nickname; ///< Literal nickname
char mode_char; ///< Op/voice/... character
bool away; ///< User is away
// XXX: maybe a good candidate for deduplication (away status)
};
static void
nick_info_destroy (void *p)
{
struct nick_info *self = p;
free (self->nickname);
free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
enum buffer_type
{
BUFFER_GLOBAL, ///< Global information
BUFFER_SERVER, ///< Server-related messages
BUFFER_CHANNEL, ///< Channels
BUFFER_PM ///< Private messages (query)
};
struct buffer
{
LIST_HEADER (struct buffer)
enum buffer_type type; ///< Type of the buffer
char *name; ///< The name of the buffer
HISTORY_STATE *history; ///< Saved history state
char *saved_line; ///< Saved line
int saved_point; ///< Saved position in line
int saved_mark; ///< Saved mark
// Buffer contents:
struct buffer_line *lines; ///< All lines in this buffer
struct buffer_line *lines_tail; ///< The tail of buffer lines
unsigned lines_count; ///< How many lines we have
unsigned unseen_messages_count; ///< # messages since last visited
// Channel information:
char *mode; ///< Channel mode
char *topic; ///< Channel topic
struct str_map nicks; ///< Maps nicks to "nick_info"
};
static struct buffer *
buffer_new (void)
{
struct buffer *self = xcalloc (1, sizeof *self);
str_map_init (&self->nicks);
self->nicks.key_xfrm = irc_strxfrm;
self->nicks.free = nick_info_destroy;
return self;
}
static void
buffer_destroy (struct buffer *self)
{
free (self->name);
// Can't really free "history" here
free (self->saved_line);
free (self->mode);
free (self->topic);
str_map_free (&self->nicks);
free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
enum color_mode
{
COLOR_AUTO, ///< Autodetect if colours are available
COLOR_ALWAYS, ///< Always use coloured output
COLOR_NEVER ///< Never use coloured output
};
struct app_context
{
// Configuration:
struct str_map config; ///< User configuration
enum color_mode color_mode; ///< Colour output mode
bool reconnect; ///< Whether to reconnect on conn. fail.
unsigned long reconnect_delay; ///< Reconnect delay in seconds
bool isolate_buffers; ///< Isolate global/server buffers
// Server connection:
int irc_fd; ///< Socket FD of the server
struct str read_buffer; ///< Input yet to be processed
struct poller_fd irc_event; ///< IRC FD event
bool irc_ready; ///< Whether we may send messages now
SSL_CTX *ssl_ctx; ///< SSL context
SSL *ssl; ///< SSL connection
// TODO: initialize and update these two values
// TODO: probably issue a USERHOST message for ourselves after connecting
// to enable proper word-wrapping; then store it here also, separately,
// without the nickname at the beginning; also could just use WHOIS
char *irc_nickname; ///< Current nickname
char *irc_user_mode; ///< Current user mode
// Events:
struct poller_fd tty_event; ///< Terminal input event
struct poller_fd signal_event; ///< Signal FD event
struct poller_timer ping_tmr; ///< We should send a ping
struct poller_timer timeout_tmr; ///< Connection seems to be dead
struct poller_timer reconnect_tmr; ///< We should reconnect now
struct poller poller; ///< Manages polled descriptors
bool quitting; ///< User requested quitting
bool polling; ///< The event loop is running
// Buffers:
struct buffer *buffers; ///< All our buffers in order
struct buffer *buffers_tail; ///< The tail of our buffers
struct str_map buffers_by_name; ///< Excludes GLOBAL and SERVER
struct buffer *global_buffer; ///< The global buffer
struct buffer *server_buffer; ///< The server buffer
struct buffer *current_buffer; ///< The current buffer
// TODO: So that we always output proper date change messages
time_t last_displayed_msg_time; ///< Time of last displayed message
// Terminal:
iconv_t term_to_utf8; ///< Terminal encoding to UTF-8
iconv_t term_from_utf8; ///< UTF-8 to terminal encoding
// XXX: shouldn't it be rather UTF-8 from Latin 1?
iconv_t term_from_latin1; ///< ISO Latin 1 to terminal encoding
int lines; ///< Current terminal height
int columns; ///< Current ternimal width
char *readline_prompt; ///< The prompt we use for readline
bool readline_prompt_shown; ///< Whether the prompt is shown now
}
*g_ctx;
static void on_irc_ping_timeout (void *user_data);
static void on_irc_timeout (void *user_data);
static void on_irc_reconnect_timeout (void *user_data);
static void
app_context_init (struct app_context *self)
{
memset (self, 0, sizeof *self);
str_map_init (&self->config);
self->config.free = free;
load_config_defaults (&self->config, g_config_table);
self->irc_fd = -1;
str_init (&self->read_buffer);
self->irc_ready = false;
str_map_init (&self->buffers_by_name);
self->buffers_by_name.key_xfrm = irc_strxfrm;
self->last_displayed_msg_time = time (NULL);
poller_init (&self->poller);
char *encoding = nl_langinfo (CODESET);
#ifdef __linux__
encoding = xstrdup_printf ("%s//TRANSLIT", encoding);
#else // ! __linux__
encoding = xstrdup (encoding);
#endif // ! __linux__
if ((self->term_from_utf8 =
iconv_open (encoding, "UTF-8")) == (iconv_t) -1
|| (self->term_from_latin1 =
iconv_open (encoding, "ISO-8859-1")) == (iconv_t) -1
|| (self->term_to_utf8 =
iconv_open ("UTF-8", nl_langinfo (CODESET))) == (iconv_t) -1)
exit_fatal ("creating the UTF-8 conversion object failed: %s",
strerror (errno));
free (encoding);
}
static void
app_context_free (struct app_context *self)
{
str_map_free (&self->config);
str_free (&self->read_buffer);
if (self->irc_fd != -1)
{
xclose (self->irc_fd);
poller_fd_reset (&self->irc_event);
}
if (self->ssl)
SSL_free (self->ssl);
if (self->ssl_ctx)
SSL_CTX_free (self->ssl_ctx);
free (self->irc_nickname);
free (self->irc_user_mode);
LIST_FOR_EACH (struct buffer, iter, self->buffers)
buffer_destroy (iter);
str_map_free (&self->buffers_by_name);
poller_free (&self->poller);
iconv_close (self->term_from_latin1);
iconv_close (self->term_from_utf8);
iconv_close (self->term_to_utf8);
free (self->readline_prompt);
}
static void refresh_prompt (struct app_context *ctx);
// --- Attributed output -------------------------------------------------------
static struct
{
bool initialized; ///< Terminal is available
bool stdout_is_tty; ///< `stdout' is a terminal
bool stderr_is_tty; ///< `stderr' is a terminal
char *color_set[8]; ///< Codes to set the foreground colour
}
g_terminal;
static bool
init_terminal (void)
{
int tty_fd = -1;
if ((g_terminal.stderr_is_tty = isatty (STDERR_FILENO)))
tty_fd = STDERR_FILENO;
if ((g_terminal.stdout_is_tty = isatty (STDOUT_FILENO)))
tty_fd = STDOUT_FILENO;
int err;
if (tty_fd == -1 || setupterm (NULL, tty_fd, &err) == ERR)
return false;
// Make sure all terminal features used by us are supported
if (!set_a_foreground || !enter_bold_mode || !exit_attribute_mode)
{
del_curterm (cur_term);
return false;
}
for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set); i++)
g_terminal.color_set[i] = xstrdup (tparm (set_a_foreground,
i, 0, 0, 0, 0, 0, 0, 0, 0));
return g_terminal.initialized = true;
}
static void
free_terminal (void)
{
if (!g_terminal.initialized)
return;
for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set); i++)
free (g_terminal.color_set[i]);
del_curterm (cur_term);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct app_readline_state
{
char *saved_line;
int saved_point;
int saved_mark;
};
static void
app_readline_hide (struct app_readline_state *state)
{
state->saved_point = rl_point;
state->saved_point = rl_mark;
state->saved_line = rl_copy_text (0, rl_end);
rl_set_prompt ("");
rl_replace_line ("", 0);
rl_redisplay ();
}
static void
app_readline_restore (struct app_readline_state *state, const char *prompt)
{
rl_set_prompt (prompt);
rl_replace_line (state->saved_line, 0);
rl_point = state->saved_point;
rl_mark = state->saved_mark;
rl_redisplay ();
free (state->saved_line);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
typedef int (*terminal_printer_fn) (int);
static int
putchar_stderr (int c)
{
return fputc (c, stderr);
}
static terminal_printer_fn
get_attribute_printer (FILE *stream)
{
if (stream == stdout && g_terminal.stdout_is_tty)
return putchar;
if (stream == stderr && g_terminal.stderr_is_tty)
return putchar_stderr;
return NULL;
}
static void
vprint_attributed (struct app_context *ctx,
FILE *stream, const char *attribute, const char *fmt, va_list ap)
{
terminal_printer_fn printer = get_attribute_printer (stream);
if (!attribute)
printer = NULL;
if (printer)
{
const char *value = str_map_find (&ctx->config, attribute);
tputs (value, 1, printer);
}
vfprintf (stream, fmt, ap);
if (printer)
{
const char *value = str_map_find (&ctx->config, ATTR_RESET);
tputs (value, 1, printer);
}
}
static void
print_attributed (struct app_context *ctx,
FILE *stream, const char *attribute, const char *fmt, ...)
{
va_list ap;
va_start (ap, fmt);
vprint_attributed (ctx, stream, attribute, fmt, ap);
va_end (ap);
}
static void
log_message_attributed (void *user_data, const char *quote, const char *fmt,
va_list ap)
{
FILE *stream = stderr;
// GNU readline is a huge piece of total crap; it seems that we must do
// these incredible shenanigans in order to intersperse readline output
// with asynchronous status messages
struct app_readline_state state;
if (g_ctx->readline_prompt_shown)
app_readline_hide (&state);
print_attributed (g_ctx, stream, user_data, "%s", quote);
vprint_attributed (g_ctx, stream, user_data, fmt, ap);
fputs ("\n", stream);
if (g_ctx->readline_prompt_shown)
app_readline_restore (&state, g_ctx->readline_prompt);
}
static void
init_colors (struct app_context *ctx)
{
// Use escape sequences from terminfo if possible, and SGR as a fallback
if (init_terminal ())
{
const char *attrs[][2] =
{
{ ATTR_PROMPT, enter_bold_mode },
{ ATTR_RESET, exit_attribute_mode },
{ ATTR_WARNING, g_terminal.color_set[3] },
{ ATTR_ERROR, g_terminal.color_set[1] },
};
for (size_t i = 0; i < N_ELEMENTS (attrs); i++)
str_map_set (&ctx->config, attrs[i][0], xstrdup (attrs[i][1]));
}
else
{
const char *attrs[][2] =
{
{ ATTR_PROMPT, "\x1b[1m" },
{ ATTR_RESET, "\x1b[0m" },
{ ATTR_WARNING, "\x1b[33m" },
{ ATTR_ERROR, "\x1b[31m" },
};
for (size_t i = 0; i < N_ELEMENTS (attrs); i++)
str_map_set (&ctx->config, attrs[i][0], xstrdup (attrs[i][1]));
}
switch (ctx->color_mode)
{
case COLOR_ALWAYS:
g_terminal.stdout_is_tty = true;
g_terminal.stderr_is_tty = true;
break;
case COLOR_AUTO:
if (!g_terminal.initialized)
{
case COLOR_NEVER:
g_terminal.stdout_is_tty = false;
g_terminal.stderr_is_tty = false;
}
}
g_log_message_real = log_message_attributed;
}
// --- Signals -----------------------------------------------------------------
static int g_signal_pipe[2]; ///< A pipe used to signal... signals
/// Program termination has been requested by a signal
static volatile sig_atomic_t g_termination_requested;
/// The window has changed in size
static volatile sig_atomic_t g_winch_received;
static void
sigterm_handler (int signum)
{
(void) signum;
g_termination_requested = true;
int original_errno = errno;
if (write (g_signal_pipe[1], "t", 1) == -1)
soft_assert (errno == EAGAIN);
errno = original_errno;
}
static void
sigwinch_handler (int signum)
{
(void) signum;
g_winch_received = true;
int original_errno = errno;
if (write (g_signal_pipe[1], "w", 1) == -1)
soft_assert (errno == EAGAIN);
errno = original_errno;
}
static void
setup_signal_handlers (void)
{
if (pipe (g_signal_pipe) == -1)
exit_fatal ("%s: %s", "pipe", strerror (errno));
set_cloexec (g_signal_pipe[0]);
set_cloexec (g_signal_pipe[1]);
// So that the pipe cannot overflow; it would make write() block within
// the signal handler, which is something we really don't want to happen.
// The same holds true for read().
set_blocking (g_signal_pipe[0], false);
set_blocking (g_signal_pipe[1], false);
signal (SIGPIPE, SIG_IGN);
struct sigaction sa;
sa.sa_flags = SA_RESTART;
sa.sa_handler = sigwinch_handler;
sigemptyset (&sa.sa_mask);
if (sigaction (SIGWINCH, &sa, NULL) == -1)
exit_fatal ("sigaction: %s", strerror (errno));
sa.sa_handler = sigterm_handler;
if (sigaction (SIGINT, &sa, NULL) == -1
|| sigaction (SIGTERM, &sa, NULL) == -1)
exit_fatal ("sigaction: %s", strerror (errno));
}
// --- Buffers -----------------------------------------------------------------
static void buffer_send (struct app_context *ctx, struct buffer *buffer,
enum buffer_line_type type, int flags, const char *origin,
const char *reason, const char *format, ...) ATTRIBUTE_PRINTF (7, 8);
static void
buffer_update_time (struct app_context *ctx, time_t now)
{
struct tm last, current;
if (!localtime_r (&ctx->last_displayed_msg_time, &last)
|| !localtime_r (&now, &current))
{
// 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", &current)))
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, &current))
print_error ("%s: %s", "localtime_r", strerror (errno));
else
str_append_printf (&text, "%02d:%02d:%02d ",
current.tm_hour, current.tm_min, current.tm_sec);
char *who = iconv_xstrdup (ctx->term_from_utf8, line->who, -1, NULL);
char *object = iconv_xstrdup (ctx->term_from_utf8, line->object, -1, NULL);
char *reason = iconv_xstrdup (ctx->term_from_utf8, line->reason, -1, NULL);
// TODO: colorize the output, note that we shouldn't put everything through
// tputs but only the attribute strings. That might prove a bit
// challenging. Maybe we could create a helper object to pust text
// and formatting into. We could have a varargs function to make it a bit
// more friendly, e.g. push(&x, ATTR_JOIN, "--> ", ATTR_RESET, who, NULL)
switch (line->type)
{
case BUFFER_LINE_PRIVMSG:
str_append_printf (&text, "<%s> %s", who, object);
break;
case BUFFER_LINE_ACTION:
str_append_printf (&text, " * %s %s", who, object);
break;
case BUFFER_LINE_NOTICE:
str_append_printf (&text, " - Notice(%s): %s", who, object);
break;
case BUFFER_LINE_JOIN:
if (who)
str_append_printf (&text, "--> %s has joined %s", who, object);
else
str_append_printf (&text, "--> You have joined %s", object);
break;
case BUFFER_LINE_PART:
if (who)
str_append_printf (&text, "<-- %s has left %s (%s)",
who, object, reason);
else
str_append_printf (&text, "<-- You have left %s (%s)",
object, reason);
break;
case BUFFER_LINE_KICK:
if (who)
str_append_printf (&text, "<-- %s has kicked %s (%s)",
who, object, reason);
else
str_append_printf (&text, "<-- You have kicked %s (%s)",
object, reason);
break;
case BUFFER_LINE_QUIT:
if (who)
str_append_printf (&text, "<-- %s has quit (%s)", who, reason);
else
str_append_printf (&text, "<-- You have quit (%s)", reason);
break;
case BUFFER_LINE_STATUS:
str_append_printf (&text, " - %s", object);
break;
case BUFFER_LINE_ERROR:
str_append_printf (&text, "=!= %s", object);
}
free (who);
free (object);
free (reason);
struct app_readline_state state;
if (ctx->readline_prompt_shown)
app_readline_hide (&state);
// TODO: write the line to a log file; note that the global and server
// buffers musn't collide with filenames
printf ("%s\n", text.str);
str_free (&text);
if (ctx->readline_prompt_shown)
app_readline_restore (&state, ctx->readline_prompt);
}
static void
buffer_send (struct app_context *ctx, struct buffer *buffer,
enum buffer_line_type type, int flags,
const char *origin, const char *reason, const char *format, ...)
{
va_list ap;
va_start (ap, format);
struct str text;
str_init (&text);
str_append_vprintf (&text, format, ap);
va_end (ap);
struct buffer_line *line = buffer_line_new ();
line->type = type;
line->flags = flags;
line->when = time (NULL);
line->who = xstrdup (origin);
line->object = str_steal (&text);
line->reason = xstrdup (reason);
LIST_APPEND_WITH_TAIL (buffer->lines, buffer->lines_tail, line);
buffer->lines_count++;
if (buffer == ctx->current_buffer)
buffer_line_display (ctx, line);
}
static struct buffer *
buffer_by_name (struct app_context *ctx, const char *name)
{
return str_map_find (&ctx->buffers_by_name, name);
}
static void
buffer_add (struct app_context *ctx, struct buffer *buffer)
{
hard_assert (!buffer_by_name (ctx, buffer->name));
str_map_set (&ctx->buffers_by_name, buffer->name, buffer);
LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
// In theory this can't cause changes in the prompt
refresh_prompt (ctx);
}
static void
buffer_remove (struct app_context *ctx, struct buffer *buffer)
{
hard_assert (buffer != ctx->current_buffer);
// rl_clear_history, being the only way I know of to get rid of the complete
// history including attached data, is a pretty recent addition. *sigh*
#if RL_READLINE_VERSION >= 0x0603
if (buffer->history)
{
// See buffer_activate() for why we need to do this BS
rl_free_undo_list ();
// This is probably the only way we can free the history fully
HISTORY_STATE *state = history_get_history_state ();
history_set_history_state (buffer->history);
rl_clear_history ();
history_set_history_state (state);
free (state);
}
#endif // RL_READLINE_VERSION
str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
buffer_destroy (buffer);
// It's not a good idea to remove these buffers, but it's even a worse
// one to leave the pointers point to invalid memory
if (buffer == ctx->global_buffer)
ctx->global_buffer = NULL;
if (buffer == ctx->server_buffer)
ctx->server_buffer = NULL;
refresh_prompt (ctx);
}
static void
buffer_activate (struct app_context *ctx, struct buffer *buffer)
{
if (ctx->current_buffer == buffer)
return;
print_status ("%s", buffer->name);
// That is, minus the buffer switch line and the readline prompt
int to_display = MAX (10, ctx->lines - 2);
struct buffer_line *line = buffer->lines_tail;
while (line && line->prev && --to_display > 0)
line = line->prev;
// Once we've found where we want to start with the backlog, print it
for (; line; line = line->next)
buffer_line_display (ctx, line);
// The following part shows you why it's not a good idea to use
// GNU Readline for this kind of software. Or for anything else, really.
// There could possibly be occurences of the current undo list in some
// history entry. We either need to free the undo list, or move it
// somewhere else to load back later, as the buffer we're switching to
// has its own history state.
rl_free_undo_list ();
// Save this buffer's history so that it's independent for each buffer
if (ctx->current_buffer)
{
ctx->current_buffer->history = history_get_history_state ();
ctx->current_buffer->saved_line = rl_copy_text (0, rl_end);
ctx->current_buffer->saved_point = rl_point;
ctx->current_buffer->saved_mark = rl_mark;
}
else
// Just throw it away; there should always be an active buffer however
#if RL_READLINE_VERSION >= 0x0603
rl_clear_history ();
#else // RL_READLINE_VERSION < 0x0603
// At least something... this may leak undo entries
clear_history ();
#endif // RL_READLINE_VERSION < 0x0603
// Now at last we can switch the pointers
ctx->current_buffer = buffer;
// Restore the target buffer's history
if (buffer->history)
{
// history_get_history_state() just allocates a new HISTORY_STATE
// and fills it with its current internal data. We don't need that
// shell anymore after reviving it.
history_set_history_state (buffer->history);
free (buffer->history);
buffer->history = NULL;
}
else
{
// This should get us a clean history while keeping the flags.
// Note that we've either saved the previous history entries, or we've
// cleared them altogether, so there should be nothing to leak.
HISTORY_STATE *state = history_get_history_state ();
state->offset = state->length = state->size = 0;
history_set_history_state (state);
free (state);
}
// Try to restore the target buffer's readline state
if (buffer->saved_line)
{
rl_replace_line (buffer->saved_line, 0);
rl_point = buffer->saved_point;
rl_mark = buffer->saved_mark;
free (buffer->saved_line);
if (ctx->readline_prompt_shown)
rl_redisplay ();
}
refresh_prompt (ctx);
}
static void
init_buffers (struct app_context *ctx)
{
// At the moment we have only two global everpresent buffers
struct buffer *global = ctx->global_buffer = buffer_new ();
struct buffer *server = ctx->server_buffer = buffer_new ();
global->type = BUFFER_GLOBAL;
global->name = xstrdup (PROGRAM_NAME);
server->type = BUFFER_SERVER;
server->name = xstrdup (str_map_find (&ctx->config, "irc_host"));
LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, global);
LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, server);
}
// --- Supporting code ---------------------------------------------------------
static void
irc_shutdown (struct app_context *ctx)
{
// TODO: set a timer after which we cut the connection?
// Generally non-critical
if (ctx->ssl)
soft_assert (SSL_shutdown (ctx->ssl) != -1);
else
soft_assert (shutdown (ctx->irc_fd, SHUT_WR) == 0);
}
static void
try_finish_quit (struct app_context *ctx)
{
if (ctx->quitting && ctx->irc_fd == -1)
ctx->polling = false;
}
static void
initiate_quit (struct app_context *ctx)
{
print_status ("shutting down");
if (ctx->irc_fd != -1)
irc_shutdown (ctx);
ctx->quitting = true;
try_finish_quit (ctx);
}
static bool irc_send (struct app_context *ctx,
const char *format, ...) ATTRIBUTE_PRINTF (2, 3);
static bool
irc_send (struct app_context *ctx, const char *format, ...)
{
va_list ap;
if (g_debug_mode)
{
struct app_readline_state state;
if (ctx->readline_prompt_shown)
app_readline_hide (&state);
fputs ("[IRC] <== \"", stderr);
va_start (ap, format);
vfprintf (stderr, format, ap);
va_end (ap);
fputs ("\"\n", stderr);
if (ctx->readline_prompt_shown)
app_readline_restore (&state, ctx->readline_prompt);
}
if (!soft_assert (ctx->irc_fd != -1))
return false;
va_start (ap, format);
struct str str;
str_init (&str);
str_append_vprintf (&str, format, ap);
str_append (&str, "\r\n");
va_end (ap);
bool result = true;
if (ctx->ssl)
{
// TODO: call SSL_get_error() to detect if a clean shutdown has occured
if (SSL_write (ctx->ssl, str.str, str.len) != (int) str.len)
{
print_debug ("%s: %s: %s", __func__, "SSL_write",
ERR_error_string (ERR_get_error (), NULL));
result = false;
}
}
else if (write (ctx->irc_fd, str.str, str.len) != (ssize_t) str.len)
{
print_debug ("%s: %s: %s", __func__, "write", strerror (errno));
result = false;
}
str_free (&str);
return result;
}
static bool
irc_get_boolean_from_config
(struct app_context *ctx, const char *name, bool *value, struct error **e)
{
const char *str = str_map_find (&ctx->config, name);
hard_assert (str != NULL);
if (set_boolean_if_valid (value, str))
return true;
error_set (e, "invalid configuration value for `%s'", name);
return false;
}
static bool
irc_initialize_ssl_ctx (struct app_context *ctx, struct error **e)
{
// XXX: maybe we should call SSL_CTX_set_options() for some workarounds
bool verify;
if (!irc_get_boolean_from_config (ctx, "ssl_verify", &verify, e))
return false;
if (!verify)
SSL_CTX_set_verify (ctx->ssl_ctx, SSL_VERIFY_NONE, NULL);
const char *ca_file = str_map_find (&ctx->config, "ca_file");
const char *ca_path = str_map_find (&ctx->config, "ca_path");
struct error *error = NULL;
if (ca_file || ca_path)
{
if (SSL_CTX_load_verify_locations (ctx->ssl_ctx, ca_file, ca_path))
return true;
error_set (&error, "%s: %s",
"failed to set locations for the CA certificate bundle",
ERR_reason_error_string (ERR_get_error ()));
goto ca_error;
}
if (!SSL_CTX_set_default_verify_paths (ctx->ssl_ctx))
{
error_set (&error, "%s: %s",
"couldn't load the default CA certificate bundle",
ERR_reason_error_string (ERR_get_error ()));
goto ca_error;
}
return true;
ca_error:
if (verify)
{
error_propagate (e, error);
return false;
}
// Only inform the user if we're not actually verifying
// FIXME: print to the server buffer
print_warning ("%s", error->message);
error_free (error);
return true;
}
static bool
irc_initialize_ssl (struct app_context *ctx, struct error **e)
{
const char *error_info = NULL;
ctx->ssl_ctx = SSL_CTX_new (SSLv23_client_method ());
if (!ctx->ssl_ctx)
goto error_ssl_1;
if (!irc_initialize_ssl_ctx (ctx, e))
goto error_ssl_2;
ctx->ssl = SSL_new (ctx->ssl_ctx);
if (!ctx->ssl)
goto error_ssl_2;
const char *ssl_cert = str_map_find (&ctx->config, "ssl_cert");
if (ssl_cert)
{
char *path = resolve_config_filename (ssl_cert);
if (!path)
// FIXME: print to the global buffer
print_error ("%s: %s", "cannot open file", ssl_cert);
// XXX: perhaps we should read the file ourselves for better messages
else if (!SSL_use_certificate_file (ctx->ssl, path, SSL_FILETYPE_PEM)
|| !SSL_use_PrivateKey_file (ctx->ssl, path, SSL_FILETYPE_PEM))
// FIXME: print to the global buffer
print_error ("%s: %s", "setting the SSL client certificate failed",
ERR_error_string (ERR_get_error (), NULL));
free (path);
}
SSL_set_connect_state (ctx->ssl);
if (!SSL_set_fd (ctx->ssl, ctx->irc_fd))
goto error_ssl_3;
// Avoid SSL_write() returning SSL_ERROR_WANT_READ
SSL_set_mode (ctx->ssl, SSL_MODE_AUTO_RETRY);
switch (xssl_get_error (ctx->ssl, SSL_connect (ctx->ssl), &error_info))
{
case SSL_ERROR_NONE:
return true;
case SSL_ERROR_ZERO_RETURN:
error_info = "server closed the connection";
default:
break;
}
error_ssl_3:
SSL_free (ctx->ssl);
ctx->ssl = NULL;
error_ssl_2:
SSL_CTX_free (ctx->ssl_ctx);
ctx->ssl_ctx = NULL;
error_ssl_1:
// XXX: these error strings are really nasty; also there could be
// multiple errors on the OpenSSL stack.
if (!error_info)
error_info = ERR_error_string (ERR_get_error (), NULL);
error_set (e, "%s: %s", "could not initialize SSL", error_info);
return false;
}
static bool
irc_establish_connection (struct app_context *ctx,
const char *host, const char *port, struct error **e)
{
struct addrinfo gai_hints, *gai_result, *gai_iter;
memset (&gai_hints, 0, sizeof gai_hints);
gai_hints.ai_socktype = SOCK_STREAM;
int err = getaddrinfo (host, port, &gai_hints, &gai_result);
if (err)
{
error_set (e, "%s: %s: %s",
"connection failed", "getaddrinfo", gai_strerror (err));
return false;
}
int sockfd;
for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next)
{
sockfd = socket (gai_iter->ai_family,
gai_iter->ai_socktype, gai_iter->ai_protocol);
if (sockfd == -1)
continue;
set_cloexec (sockfd);
int yes = 1;
soft_assert (setsockopt (sockfd, SOL_SOCKET, SO_KEEPALIVE,
&yes, sizeof yes) != -1);
const char *real_host = host;
// Let's try to resolve the address back into a real hostname;
// we don't really need this, so we can let it quietly fail
char buf[NI_MAXHOST];
err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen,
buf, sizeof buf, NULL, 0, NI_NUMERICHOST);
if (err)
print_debug ("%s: %s", "getnameinfo", gai_strerror (err));
else
real_host = buf;
char *address = format_host_port_pair (real_host, port);
// FIXME: print to the server buffer
print_status ("connecting to %s...", address);
free (address);
if (!connect (sockfd, gai_iter->ai_addr, gai_iter->ai_addrlen))
break;
xclose (sockfd);
}
freeaddrinfo (gai_result);
if (!gai_iter)
{
error_set (e, "connection failed");
return false;
}
ctx->irc_fd = sockfd;
return true;
}
// --- More readline funky stuff -----------------------------------------------
static char *
get_unseen_prefix (struct app_context *ctx)
{
struct str active_buffers;
str_init (&active_buffers);
size_t i = 0;
LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
{
i++;
if (!iter->unseen_messages_count)
continue;
if (active_buffers.len)
str_append_c (&active_buffers, ',');
str_append_printf (&active_buffers, "%zu", i);
}
if (active_buffers.len)
return str_steal (&active_buffers);
str_free (&active_buffers);
return NULL;
}
static void
refresh_prompt (struct app_context *ctx)
{
bool have_attributes = !!get_attribute_printer (stdout);
struct str prompt;
str_init (&prompt);
if (!ctx->irc_ready)
str_append (&prompt, "(disconnected)");
else if (soft_assert (ctx->current_buffer))
{
struct buffer *buffer = ctx->current_buffer;
str_append_c (&prompt, '[');
char *unseen_prefix = get_unseen_prefix (ctx);
if (unseen_prefix)
str_append_printf (&prompt, "(%s) ", unseen_prefix);
free (unseen_prefix);
str_append (&prompt, buffer->name);
if (buffer->type == BUFFER_CHANNEL)
str_append_printf (&prompt, "(%s)", buffer->mode);
str_append_c (&prompt, ' ');
str_append (&prompt, ctx->irc_nickname);
str_append_printf (&prompt, "(%s)", ctx->irc_user_mode);
str_append_c (&prompt, ']');
}
str_append (&prompt, " ");
// After building the new prompt, replace the old one
free (ctx->readline_prompt);
if (!have_attributes)
ctx->readline_prompt = xstrdup (prompt.str);
else
{
// XXX: to be completely correct, we should use tputs, but we cannot
const char *prompt_attrs = str_map_find (&ctx->config, ATTR_PROMPT);
const char *reset_attrs = str_map_find (&ctx->config, ATTR_RESET);
ctx->readline_prompt = xstrdup_printf ("%c%s%c%s%c%s%c",
RL_PROMPT_START_IGNORE, prompt_attrs, RL_PROMPT_END_IGNORE,
prompt.str,
RL_PROMPT_START_IGNORE, reset_attrs, RL_PROMPT_END_IGNORE);
}
str_free (&prompt);
// We need to be somehow able to initialize it
rl_set_prompt (ctx->readline_prompt);
if (ctx->readline_prompt_shown)
rl_redisplay ();
}
static int
on_readline_goto_buffer (int count, int key)
{
(void) count;
if (!(key & 0x80))
return 0;
int n = (key & 0x7F) - '0';
if (n < 0 || n > 9)
return 0;
// There's no zero-th buffer
if (n == 0)
n = 10;
// Activate the n-th buffer
int i = 0;
struct app_context *ctx = g_ctx;
LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
if (++i == n)
{
buffer_activate (ctx, iter);
break;
}
return 0;
}
static int
on_readline_previous_buffer (int count, int key)
{
(void) key;
struct app_context *ctx = g_ctx;
if (!ctx->current_buffer)
return 0;
struct buffer *new_buffer = ctx->current_buffer;
while (count-- > 0)
if (!(new_buffer = new_buffer->prev))
new_buffer = ctx->buffers_tail;
buffer_activate (ctx, new_buffer);
return 0;
}
static int
on_readline_next_buffer (int count, int key)
{
(void) key;
struct app_context *ctx = g_ctx;
if (!ctx->current_buffer)
return 0;
struct buffer *new_buffer = ctx->current_buffer;
while (count-- > 0)
if (!(new_buffer = new_buffer->next))
new_buffer = ctx->buffers;
buffer_activate (ctx, new_buffer);
return 0;
}
static int
init_readline (void)
{
// TODO: maybe use rl_make_bare_keymap() and start from there;
// our dear user could potentionally rig things up in a way that might
// result in some funny unspecified behaviour
rl_add_defun ("previous-buffer", on_readline_previous_buffer, -1);
rl_add_defun ("next-buffer", on_readline_next_buffer, -1);
// Redefine M-0 through M-9 to switch buffers
for (int i = 0; i <= 9; i++)
rl_bind_key (0x80 /* this is the Meta modifier for Readline */
| ('0' + i), on_readline_goto_buffer);
rl_bind_keyseq ("C-p", rl_named_function ("previous-buffer"));
rl_bind_keyseq ("C-n", rl_named_function ("next-buffer"));
rl_bind_keyseq ("M-p", rl_named_function ("previous-history"));
rl_bind_keyseq ("M-n", rl_named_function ("next-history"));
return 0;
}
// --- Input handling ----------------------------------------------------------
// TODO: we will need a proper mode parser; to be shared with kike
// TODO: we alse definitely need to parse server capability messages
static void
irc_handle_ping (struct app_context *ctx, const struct irc_message *msg)
{
if (msg->params.len)
irc_send (ctx, "PONG :%s", msg->params.vector[0]);
else
irc_send (ctx, "PONG");
}
static struct irc_handler
{
char *name;
void (*handler) (struct app_context *ctx, const struct irc_message *msg);
}
g_irc_handlers[] =
{
// This list needs to stay sorted
// TODO: handle as much as we can
{ "PING", irc_handle_ping },
};
static int
irc_handler_cmp_by_name (const void *a, const void *b)
{
const struct irc_handler *first = a;
const struct irc_handler *second = b;
return strcasecmp_ascii (first->name, second->name);
}
static void
irc_process_message (const struct irc_message *msg,
const char *raw, void *user_data)
{
struct app_context *ctx = user_data;
if (g_debug_mode)
{
struct app_readline_state state;
if (ctx->readline_prompt_shown)
app_readline_hide (&state);
// TODO: ensure proper encoding
fprintf (stderr, "[%s] ==> \"%s\"\n", "IRC", raw);
if (ctx->readline_prompt_shown)
app_readline_restore (&state, ctx->readline_prompt);
}
if (!ctx->irc_ready && (!strcasecmp (msg->command, "MODE")
|| !strcasecmp (msg->command, "376") // RPL_ENDOFMOTD
|| !strcasecmp (msg->command, "422"))) // ERR_NOMOTD
{
// FIXME: print to the server buffer
print_status ("successfully connected");
ctx->irc_ready = true;
const char *autojoin = str_map_find (&ctx->config, "autojoin");
if (autojoin)
irc_send (ctx, "JOIN :%s", autojoin);
}
struct irc_handler key = { .name = msg->command };
struct irc_handler *handler = bsearch (&key, g_irc_handlers,
N_ELEMENTS (g_irc_handlers), sizeof key, irc_handler_cmp_by_name);
if (handler)
handler->handler (ctx, msg);
// Numerics typically have human-readable information
unsigned long dummy;
if (xstrtoul (&dummy, msg->command, 10))
// TODO: ensure proper encoding
// FIXME: print to the server buffer
print_status ("%s", raw);
}
// --- User input handling -----------------------------------------------------
static void handle_command_help (struct app_context *, const char *);
static void
handle_command_buffer (struct app_context *ctx, const char *arguments)
{
// TODO: parse the arguments
}
static void
handle_command_quit (struct app_context *ctx, const char *arguments)
{
if (ctx->irc_fd != -1)
{
if (*arguments)
irc_send (ctx, "QUIT :%s", arguments);
else
irc_send (ctx, "QUIT :%s", PROGRAM_NAME " " PROGRAM_VERSION);
}
initiate_quit (ctx);
}
static struct command_handler
{
char *name;
void (*handler) (struct app_context *ctx, const char *arguments);
// TODO: probably also a usage string
}
g_command_handlers[] =
{
{ "help", handle_command_help },
{ "quit", handle_command_quit },
{ "buffer", handle_command_buffer },
#if 0
{ "msg", NULL },
{ "query", NULL },
{ "notice", NULL },
{ "ctcp", NULL },
{ "me", NULL },
{ "join", NULL },
{ "part", NULL },
{ "cycle", NULL },
{ "mode", NULL },
{ "topic", NULL },
{ "kick", NULL },
{ "kickban", NULL },
{ "ban", NULL },
{ "invite", NULL },
{ "list", NULL },
{ "names", NULL },
{ "who", NULL },
{ "whois", NULL },
{ "motd", NULL },
{ "away", NULL },
{ "quote", NULL },
#endif
};
static void
handle_command_help (struct app_context *ctx, const char *arguments)
{
// TODO: show a list of all user commands
}
static int
command_handler_cmp_by_length (const void *a, const void *b)
{
const struct command_handler *first = a;
const struct command_handler *second = b;
return strlen (first->name) - strlen (second->name);
}
static void
init_partial_matching_user_command_map (struct str_map *partial)
{
str_map_init (partial);
partial->key_xfrm = tolower_ascii_strxfrm;
// We process them from the longest to the shortest one,
// so that common prefixes favor shorter entries
struct command_handler *by_length[N_ELEMENTS (g_command_handlers)];
for (size_t i = 0; i < N_ELEMENTS (by_length); i++)
by_length[i] = &g_command_handlers[i];
qsort (by_length, N_ELEMENTS (by_length), sizeof *by_length,
command_handler_cmp_by_length);
for (size_t i = N_ELEMENTS (by_length); i--; )
{
char *copy = xstrdup (by_length[i]->name);
for (size_t part = strlen (copy); part; part--)
{
copy[part] = '\0';
str_map_set (partial, copy, by_length[i]);
}
free (copy);
}
}
static void
process_user_command (struct app_context *ctx, char *command)
{
// Trivially create a partial matching map
static bool initialized = false;
struct str_map partial;
if (!initialized)
{
init_partial_matching_user_command_map (&partial);
initialized = true;
}
// TODO: cut a single word (strtok_r()?)
// TODO: if it's a number, switch to the given buffer
struct command_handler *handler = str_map_find (&partial, command);
if (handler)
// FIXME: pass arguments correctly
handler->handler (ctx, "");
}
static void
send_message_to_current_buffer (struct app_context *ctx, char *message)
{
struct buffer *buffer = ctx->current_buffer;
if (!buffer)
{
// TODO: print an error message to the global buffer
return;
}
switch (buffer->type)
{
case BUFFER_GLOBAL:
case BUFFER_SERVER:
// TODO: print a message to the buffer that it's not a channel
break;
case BUFFER_CHANNEL:
// TODO: send an IRC message to the channel
break;
case BUFFER_PM:
// TODO: send an IRC message to the user
break;
}
}
static void
process_input (struct app_context *ctx, char *user_input)
{
char *input;
size_t len;
if (!(input = iconv_xstrdup (ctx->term_to_utf8, user_input, -1, &len)))
print_error ("character conversion failed for `%s'", "user input");
else if (input[0] == '/' && input[1] != '/')
process_user_command (ctx, input + 1);
else
send_message_to_current_buffer (ctx, input);
free (input);
}
// --- Supporting code (continued) ---------------------------------------------
enum irc_read_result
{
IRC_READ_OK, ///< Some data were read successfully
IRC_READ_EOF, ///< The server has closed connection
IRC_READ_AGAIN, ///< No more data at the moment
IRC_READ_ERROR ///< General connection failure
};
static enum irc_read_result
irc_fill_read_buffer_ssl (struct app_context *ctx, struct str *buf)
{
int n_read;
start:
n_read = SSL_read (ctx->ssl, buf->str + buf->len,
buf->alloc - buf->len - 1 /* null byte */);
const char *error_info = NULL;
switch (xssl_get_error (ctx->ssl, n_read, &error_info))
{
case SSL_ERROR_NONE:
buf->str[buf->len += n_read] = '\0';
return IRC_READ_OK;
case SSL_ERROR_ZERO_RETURN:
return IRC_READ_EOF;
case SSL_ERROR_WANT_READ:
return IRC_READ_AGAIN;
case SSL_ERROR_WANT_WRITE:
{
// Let it finish the handshake as we don't poll for writability;
// any errors are to be collected by SSL_read() in the next iteration
struct pollfd pfd = { .fd = ctx->irc_fd, .events = POLLOUT };
soft_assert (poll (&pfd, 1, 0) > 0);
goto start;
}
case XSSL_ERROR_TRY_AGAIN:
goto start;
default:
print_debug ("%s: %s: %s", __func__, "SSL_read", error_info);
return IRC_READ_ERROR;
}
}
static enum irc_read_result
irc_fill_read_buffer (struct app_context *ctx, struct str *buf)
{
ssize_t n_read;
start:
n_read = recv (ctx->irc_fd, buf->str + buf->len,
buf->alloc - buf->len - 1 /* null byte */, 0);
if (n_read > 0)
{
buf->str[buf->len += n_read] = '\0';
return IRC_READ_OK;
}
if (n_read == 0)
return IRC_READ_EOF;
if (errno == EAGAIN)
return IRC_READ_AGAIN;
if (errno == EINTR)
goto start;
print_debug ("%s: %s: %s", __func__, "recv", strerror (errno));
return IRC_READ_ERROR;
}
static bool irc_connect (struct app_context *, struct error **);
static void irc_queue_reconnect (struct app_context *);
static void
irc_cancel_timers (struct app_context *ctx)
{
poller_timer_reset (&ctx->timeout_tmr);
poller_timer_reset (&ctx->ping_tmr);
poller_timer_reset (&ctx->reconnect_tmr);
}
static void
on_irc_reconnect_timeout (void *user_data)
{
struct app_context *ctx = user_data;
struct error *e = NULL;
if (irc_connect (ctx, &e))
return;
// FIXME: print to the server buffer
print_error ("%s", e->message);
error_free (e);
irc_queue_reconnect (ctx);
}
static void
irc_queue_reconnect (struct app_context *ctx)
{
hard_assert (ctx->irc_fd == -1);
// FIXME: print to the server buffer
print_status ("trying to reconnect in %ld seconds...",
ctx->reconnect_delay);
poller_timer_set (&ctx->reconnect_tmr, ctx->reconnect_delay * 1000);
}
static void
on_irc_disconnected (struct app_context *ctx)
{
// Get rid of the dead socket and related things
if (ctx->ssl)
{
SSL_free (ctx->ssl);
ctx->ssl = NULL;
SSL_CTX_free (ctx->ssl_ctx);
ctx->ssl_ctx = NULL;
}
xclose (ctx->irc_fd);
ctx->irc_fd = -1;
ctx->irc_ready = false;
ctx->irc_event.closed = true;
poller_fd_reset (&ctx->irc_event);
// All of our timers have lost their meaning now
irc_cancel_timers (ctx);
if (ctx->quitting)
try_finish_quit (ctx);
else if (!ctx->reconnect)
initiate_quit (ctx);
else
irc_queue_reconnect (ctx);
}
static void
on_irc_ping_timeout (void *user_data)
{
struct app_context *ctx = user_data;
// FIXME: print to the server buffer
print_error ("connection timeout");
on_irc_disconnected (ctx);
}
static void
on_irc_timeout (void *user_data)
{
// Provoke a response from the server
struct app_context *ctx = user_data;
irc_send (ctx, "PING :%s",
(char *) str_map_find (&ctx->config, "nickname"));
}
static void
irc_reset_connection_timeouts (struct app_context *ctx)
{
irc_cancel_timers (ctx);
poller_timer_set (&ctx->timeout_tmr, 3 * 60 * 1000);
poller_timer_set (&ctx->ping_tmr, (3 * 60 + 30) * 1000);
}
static void
on_irc_readable (const struct pollfd *fd, struct app_context *ctx)
{
if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
(void) set_blocking (ctx->irc_fd, false);
struct str *buf = &ctx->read_buffer;
enum irc_read_result (*fill_buffer)(struct app_context *, struct str *)
= ctx->ssl
? irc_fill_read_buffer_ssl
: irc_fill_read_buffer;
bool disconnected = false;
while (true)
{
str_ensure_space (buf, 512);
switch (fill_buffer (ctx, buf))
{
case IRC_READ_AGAIN:
goto end;
case IRC_READ_ERROR:
// FIXME: print to the server buffer
print_error ("reading from the IRC server failed");
disconnected = true;
goto end;
case IRC_READ_EOF:
// FIXME: print to the server buffer
print_status ("the IRC server closed the connection");
disconnected = true;
goto end;
case IRC_READ_OK:
break;
}
if (buf->len >= (1 << 20))
{
// FIXME: print to the server buffer
print_error ("the IRC server seems to spew out data frantically");
irc_shutdown (ctx);
goto end;
}
}
end:
(void) set_blocking (ctx->irc_fd, true);
irc_process_buffer (buf, irc_process_message, ctx);
if (disconnected)
on_irc_disconnected (ctx);
else
irc_reset_connection_timeouts (ctx);
}
static bool
irc_connect (struct app_context *ctx, struct error **e)
{
const char *irc_host = str_map_find (&ctx->config, "irc_host");
const char *irc_port = str_map_find (&ctx->config, "irc_port");
const char *socks_host = str_map_find (&ctx->config, "socks_host");
const char *socks_port = str_map_find (&ctx->config, "socks_port");
const char *socks_username = str_map_find (&ctx->config, "socks_username");
const char *socks_password = str_map_find (&ctx->config, "socks_password");
const char *nickname = str_map_find (&ctx->config, "nickname");
const char *username = str_map_find (&ctx->config, "username");
const char *realname = str_map_find (&ctx->config, "realname");
// We have a default value for these
hard_assert (irc_port && socks_port);
// These are filled automatically if needed
hard_assert (nickname && username && realname);
// TODO: again, get rid of `struct error' in here. The question is: how
// do we tell our caller that he should not try to reconnect?
if (!irc_host)
{
error_set (e, "no hostname specified in configuration");
return false;
}
bool use_ssl;
if (!irc_get_boolean_from_config (ctx, "ssl", &use_ssl, e))
return false;
if (socks_host)
{
char *address = format_host_port_pair (irc_host, irc_port);
char *socks_address = format_host_port_pair (socks_host, socks_port);
// FIXME: print to the server buffer
print_status ("connecting to %s via %s...", address, socks_address);
free (socks_address);
free (address);
struct error *error = NULL;
int fd = socks_connect (socks_host, socks_port, irc_host, irc_port,
socks_username, socks_password, &error);
if (fd == -1)
{
error_set (e, "%s: %s", "SOCKS connection failed", error->message);
error_free (error);
return false;
}
ctx->irc_fd = fd;
}
else if (!irc_establish_connection (ctx, irc_host, irc_port, e))
return false;
if (use_ssl && !irc_initialize_ssl (ctx, e))
{
xclose (ctx->irc_fd);
ctx->irc_fd = -1;
return false;
}
// FIXME: print to the server buffer
print_status ("connection established");
poller_fd_init (&ctx->irc_event, &ctx->poller, ctx->irc_fd);
ctx->irc_event.dispatcher = (poller_fd_fn) on_irc_readable;
ctx->irc_event.user_data = ctx;
poller_fd_set (&ctx->irc_event, POLLIN);
irc_reset_connection_timeouts (ctx);
irc_send (ctx, "NICK %s", nickname);
irc_send (ctx, "USER %s 8 * :%s", username, realname);
return true;
}
// --- I/O event handlers ------------------------------------------------------
static void
on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx)
{
char dummy;
(void) read (fd->fd, &dummy, 1);
if (g_termination_requested && !ctx->quitting)
{
// There may be a timer set to reconnect to the server
irc_cancel_timers (ctx);
if (ctx->irc_fd != -1)
irc_send (ctx, "QUIT :Terminated by signal");
initiate_quit (ctx);
}
if (g_winch_received)
{
// This fucks up big time on terminals with automatic wrapping such as
// rxvt-unicode or newer VTE when the current line overflows, however we
// can't do much about that
rl_resize_terminal ();
rl_get_screen_size (&ctx->lines, &ctx->columns);
}
}
static void
on_tty_readable (const struct pollfd *fd, struct app_context *ctx)
{
(void) ctx;
if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
rl_callback_read_char ();
}
static void
on_readline_input (char *line)
{
// Otherwise the prompt is shown at all times
g_ctx->readline_prompt_shown = false;
if (line)
{
if (*line)
add_history (line);
process_input (g_ctx, line);
free (line);
}
g_ctx->readline_prompt_shown = true;
}
// --- Configuration loading ---------------------------------------------------
static bool
read_hexa_escape (const char **cursor, struct str *output)
{
int i;
char c, code = 0;
for (i = 0; i < 2; i++)
{
c = tolower (*(*cursor));
if (c >= '0' && c <= '9')
code = (code << 4) | (c - '0');
else if (c >= 'a' && c <= 'f')
code = (code << 4) | (c - 'a' + 10);
else
break;
(*cursor)++;
}
if (!i)
return false;
str_append_c (output, code);
return true;
}
static bool
read_octal_escape (const char **cursor, struct str *output)
{
int i;
char c, code = 0;
for (i = 0; i < 3; i++)
{
c = *(*cursor);
if (c < '0' || c > '7')
break;
code = (code << 3) | (c - '0');
(*cursor)++;
}
if (!i)
return false;
str_append_c (output, code);
return true;
}
static bool
read_string_escape_sequence (const char **cursor,
struct str *output, struct error **e)
{
int c;
switch ((c = *(*cursor)++))
{
case '?': str_append_c (output, '?'); break;
case '"': str_append_c (output, '"'); break;
case '\\': str_append_c (output, '\\'); break;
case 'a': str_append_c (output, '\a'); break;
case 'b': str_append_c (output, '\b'); break;
case 'f': str_append_c (output, '\f'); break;
case 'n': str_append_c (output, '\n'); break;
case 'r': str_append_c (output, '\r'); break;
case 't': str_append_c (output, '\t'); break;
case 'v': str_append_c (output, '\v'); break;
case 'e':
case 'E':
str_append_c (output, '\x1b');
break;
case 'x':
case 'X':
if (!read_hexa_escape (cursor, output))
FAIL ("invalid hexadecimal escape");
break;
case '\0':
FAIL ("premature end of escape sequence");
default:
(*cursor)--;
if (!read_octal_escape (cursor, output))
FAIL ("unknown escape sequence");
}
return true;
}
static bool
unescape_string (const char *s, struct str *output, struct error **e)
{
int c;
while ((c = *s++))
{
if (c != '\\')
str_append_c (output, c);
else if (!read_string_escape_sequence (&s, output, e))
return false;
}
return true;
}
static bool
autofill_user_info (struct app_context *ctx, struct error **e)
{
const char *nickname = str_map_find (&ctx->config, "nickname");
const char *username = str_map_find (&ctx->config, "username");
const char *realname = str_map_find (&ctx->config, "realname");
if (nickname && username && realname)
return true;
// TODO: read POSIX user info and fill the configuration if needed
struct passwd *pwd = getpwuid (geteuid ());
if (!pwd)
FAIL ("cannot retrieve user information: %s", strerror (errno));
if (!nickname)
str_map_set (&ctx->config, "nickname", xstrdup (pwd->pw_name));
if (!username)
str_map_set (&ctx->config, "username", xstrdup (pwd->pw_name));
// Not all systems have the GECOS field but the vast majority does
if (!realname)
{
char *gecos = pwd->pw_gecos;
// The first comma, if any, ends the user's real name
char *comma = strchr (gecos, ',');
if (comma)
*comma = '\0';
str_map_set (&ctx->config, "username", xstrdup (gecos));
}
return true;
}
static bool
unescape_config (struct str_map *input, struct str_map *output, struct error **e)
{
struct error *error = NULL;
struct str_map_iter iter;
str_map_iter_init (&iter, input);
while (str_map_iter_next (&iter))
{
struct str value;
str_init (&value);
if (!unescape_string (iter.link->data, &value, &error))
{
error_set (e, "error reading configuration: %s: %s",
iter.link->key, error->message);
error_free (error);
return false;
}
str_map_set (output, iter.link->key, str_steal (&value));
}
return true;
}
static bool
load_config (struct app_context *ctx, struct error **e)
{
// TODO: employ a better configuration file format, so that we don't have
// to do this convoluted post-processing anymore.
struct str_map map;
str_map_init (&map);
map.free = free;
bool success = read_config_file (&map, e) &&
unescape_config (&map, &ctx->config, e) &&
autofill_user_info (ctx, e);
str_map_free (&map);
if (!success)
return false;
if (!irc_get_boolean_from_config (ctx,
"reconnect", &ctx->reconnect, e)
|| !irc_get_boolean_from_config (ctx,
"isolate_buffers", &ctx->isolate_buffers, e))
return false;
const char *delay_str = str_map_find (&ctx->config, "reconnect_delay");
hard_assert (delay_str != NULL); // We have a default value for this
if (!xstrtoul (&ctx->reconnect_delay, delay_str, 10))
{
error_set (e, "invalid configuration value for `%s'",
"reconnect_delay");
return false;
}
return true;
}
// --- Main program ------------------------------------------------------------
static void
init_poller_events (struct app_context *ctx)
{
poller_timer_init (&ctx->timeout_tmr, &ctx->poller);
ctx->timeout_tmr.dispatcher = on_irc_timeout;
ctx->timeout_tmr.user_data = ctx;
poller_timer_init (&ctx->ping_tmr, &ctx->poller);
ctx->ping_tmr.dispatcher = on_irc_ping_timeout;
ctx->ping_tmr.user_data = ctx;
poller_timer_init (&ctx->reconnect_tmr, &ctx->poller);
ctx->reconnect_tmr.dispatcher = on_irc_reconnect_timeout;
ctx->reconnect_tmr.user_data = ctx;
poller_fd_init (&ctx->signal_event, &ctx->poller, g_signal_pipe[0]);
ctx->signal_event.dispatcher = (poller_fd_fn) on_signal_pipe_readable;
ctx->signal_event.user_data = &ctx;
poller_fd_set (&ctx->signal_event, POLLIN);
poller_fd_init (&ctx->tty_event, &ctx->poller, STDIN_FILENO);
ctx->tty_event.dispatcher = (poller_fd_fn) on_tty_readable;
ctx->tty_event.user_data = &ctx;
poller_fd_set (&ctx->tty_event, POLLIN);
}
int
main (int argc, char *argv[])
{
static const struct opt opts[] =
{
{ 'd', "debug", NULL, 0, "run in debug mode" },
{ 'h', "help", NULL, 0, "display this help and exit" },
{ 'V', "version", NULL, 0, "output version information and exit" },
{ 'w', "write-default-cfg", "FILENAME",
OPT_OPTIONAL_ARG | OPT_LONG_ONLY,
"write a default configuration file and exit" },
{ 0, NULL, NULL, 0, NULL }
};
struct opt_handler oh;
opt_handler_init (&oh, argc, argv, opts, NULL, "Experimental IRC client.");
int c;
while ((c = opt_handler_get (&oh)) != -1)
switch (c)
{
case 'd':
g_debug_mode = true;
break;
case 'h':
opt_handler_usage (&oh, stdout);
exit (EXIT_SUCCESS);
case 'V':
printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
exit (EXIT_SUCCESS);
case 'w':
call_write_default_config (optarg, g_config_table);
exit (EXIT_SUCCESS);
default:
print_error ("wrong options");
opt_handler_usage (&oh, stderr);
exit (EXIT_FAILURE);
}
opt_handler_free (&oh);
struct app_context ctx;
print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting");
app_context_init (&ctx);
g_ctx = &ctx;
// We only need to convert to and from the terminal encoding
setlocale (LC_CTYPE, "");
SSL_library_init ();
atexit (EVP_cleanup);
SSL_load_error_strings ();
atexit (ERR_free_strings);
using_history ();
// This can cause memory leaks, or maybe even a segfault. Funny, eh?
stifle_history (HISTORY_LIMIT);
setup_signal_handlers ();
init_colors (&ctx);
init_poller_events (&ctx);
init_buffers (&ctx);
ctx.current_buffer = ctx.global_buffer;
refresh_prompt (&ctx);
// TODO: connect asynchronously (first step towards multiple servers)
// TODO: print load_config() errors to the global buffer,
// switch buffers and print irc_connect() errors to the server buffer?
struct error *e = NULL;
if (!load_config (&ctx, &e)
|| !irc_connect (&ctx, &e))
{
// FIXME: print to the global buffer
print_error ("%s", e->message);
error_free (e);
exit (EXIT_FAILURE);
}
rl_startup_hook = init_readline;
rl_catch_sigwinch = false;
rl_callback_handler_install (ctx.readline_prompt, on_readline_input);
rl_get_screen_size (&ctx.lines, &ctx.columns);
ctx.readline_prompt_shown = true;
ctx.polling = true;
while (ctx.polling)
poller_run (&ctx.poller);
if (ctx.readline_prompt_shown)
rl_callback_handler_remove ();
putchar ('\n');
app_context_free (&ctx);
free_terminal ();
return EXIT_SUCCESS;
}