xK/kike.c
Přemysl Eric Janouch 383f6af344
Improve OpenSSL integration
Ensure the error stack is cleared after errors are processed.

Also handle NULL returns safely.

Makes the debug mode spew more data, though almost none of
the contexts is in reaction to network peer data.
2020-10-20 01:55:46 +02:00

4062 lines
113 KiB
C

/*
* kike.c: the experimental IRC daemon
*
* Copyright (c) 2014 - 2018, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* 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.
*
*/
#include "config.h"
#define PROGRAM_NAME "kike"
#define WANT_SYSLOG_LOGGING
#include "common.c"
#include "kike-replies.c"
#include <nl_types.h>
enum { PIPE_READ, PIPE_WRITE };
// FIXME: don't use time_t to compute time deltas
// --- Configuration (application-specific) ------------------------------------
// Just get rid of the crappiest ciphers available by default
#define DEFAULT_CIPHERS "DEFAULT:!MEDIUM:!LOW"
static struct simple_config_item g_config_table[] =
{
{ "pid_file", NULL, "Path or name of the PID file" },
{ "server_name", NULL, "Server name" },
{ "server_info", "My server", "Brief server description" },
{ "motd", NULL, "MOTD filename" },
{ "catalog", NULL, "catgets localization catalog" },
{ "bind_host", NULL, "Address of the IRC server" },
{ "bind_port", "6667", "Port of the IRC server" },
{ "tls_cert", NULL, "Server TLS certificate (PEM)" },
{ "tls_key", NULL, "Server TLS private key (PEM)" },
{ "tls_ciphers", DEFAULT_CIPHERS, "OpenSSL cipher list" },
{ "operators", NULL, "IRCop TLS cert. fingerprints" },
{ "max_connections", "0", "Global connection limit" },
{ "ping_interval", "180", "Interval between PINGs (sec)" },
{ NULL, NULL, NULL }
};
// --- 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;
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
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;
sigemptyset (&sa.sa_mask);
sa.sa_handler = sigterm_handler;
if (sigaction (SIGINT, &sa, NULL) == -1
|| sigaction (SIGTERM, &sa, NULL) == -1)
exit_fatal ("%s: %s", "sigaction", strerror (errno));
}
// --- Rate limiter ------------------------------------------------------------
struct flood_detector
{
unsigned interval; ///< Interval for the limit
unsigned limit; ///< Maximum number of events allowed
time_t *timestamps; ///< Timestamps of last events
unsigned pos; ///< Index of the oldest event
};
static void
flood_detector_init (struct flood_detector *self,
unsigned interval, unsigned limit)
{
self->interval = interval;
self->limit = limit;
self->timestamps = xcalloc (limit + 1, sizeof *self->timestamps);
self->pos = 0;
}
static void
flood_detector_free (struct flood_detector *self)
{
free (self->timestamps);
}
static bool
flood_detector_check (struct flood_detector *self)
{
time_t now = time (NULL);
self->timestamps[self->pos++] = now;
if (self->pos > self->limit)
self->pos = 0;
time_t begin = now - self->interval;
size_t count = 0;
for (size_t i = 0; i <= self->limit; i++)
if (self->timestamps[i] >= begin)
count++;
return count <= self->limit;
}
// --- IRC token validation ----------------------------------------------------
// Use the enum only if applicable and a simple boolean isn't sufficient.
enum validation_result
{
VALIDATION_OK,
VALIDATION_ERROR_EMPTY,
VALIDATION_ERROR_TOO_LONG,
VALIDATION_ERROR_INVALID
};
// Everything as per RFC 2812
#define IRC_MAX_NICKNAME 9
#define IRC_MAX_HOSTNAME 63
#define IRC_MAX_CHANNEL_NAME 50
#define IRC_MAX_MESSAGE_LENGTH 510
static bool
irc_regex_match (const char *regex, const char *s)
{
static struct str_map cache;
static bool initialized;
if (!initialized)
{
cache = regex_cache_make ();
initialized = true;
}
struct error *e = NULL;
bool result = regex_cache_match (&cache, regex,
REG_EXTENDED | REG_NOSUB, s, &e);
hard_assert (!e);
return result;
}
static const char *
irc_validate_to_str (enum validation_result result)
{
switch (result)
{
case VALIDATION_OK: return "success";
case VALIDATION_ERROR_EMPTY: return "the value is empty";
case VALIDATION_ERROR_INVALID: return "invalid format";
case VALIDATION_ERROR_TOO_LONG: return "the value is too long";
default: abort ();
}
}
// Anything to keep it as short as possible
// "shortname" from RFC 2812 doesn't work how its author thought it would.
#define SN "[0-9A-Za-z](-*[0-9A-Za-z])*"
#define N4 "[0-9]{1,3}"
#define N6 "[0-9ABCDEFabcdef]{1,}"
#define LE "A-Za-z"
#define SP "][\\\\`_^{|}"
static enum validation_result
irc_validate_hostname (const char *hostname)
{
if (!*hostname)
return VALIDATION_ERROR_EMPTY;
if (!irc_regex_match ("^" SN "(\\." SN ")*$", hostname))
return VALIDATION_ERROR_INVALID;
if (strlen (hostname) > IRC_MAX_HOSTNAME)
return VALIDATION_ERROR_TOO_LONG;
return VALIDATION_OK;
}
static bool
irc_is_valid_hostaddr (const char *hostaddr)
{
if (irc_regex_match ("^" N4 "\\." N4 "\\." N4 "\\." N4 "$", hostaddr)
|| irc_regex_match ("^" N6 ":" N6 ":" N6 ":" N6 ":"
N6 ":" N6 ":" N6 ":" N6 "$", hostaddr)
|| irc_regex_match ("^0:0:0:0:0:(0|[Ff]{4}):"
N4 "\\." N4 "\\." N4 "\\." N4 "$", hostaddr))
return true;
return false;
}
// TODO: we should actually use this, though what should we do on failure?
static bool
irc_is_valid_host (const char *host)
{
return irc_validate_hostname (host) == VALIDATION_OK
|| irc_is_valid_hostaddr (host);
}
// TODO: currently, we are almost encoding-agnostic (strings just need to be
// ASCII-compatible). We should at least have an option to enforce a specific
// encoding, such as UTF-8. Note that with Unicode we should not allow all
// character clasess and exclude the likes of \pM with the goal of enforcing
// NFC-normalized identifiers--utf8proc is a good candidate library to handle
// the categorization and validation.
static bool
irc_is_valid_user (const char *user)
{
return irc_regex_match ("^[^\r\n @]+$", user);
}
static bool
irc_validate_nickname (const char *nickname)
{
if (!*nickname)
return VALIDATION_ERROR_EMPTY;
if (!irc_regex_match ("^[" SP LE "][" SP LE "0-9-]*$", nickname))
return VALIDATION_ERROR_INVALID;
if (strlen (nickname) > IRC_MAX_NICKNAME)
return VALIDATION_ERROR_TOO_LONG;
return VALIDATION_OK;
}
static enum validation_result
irc_validate_channel_name (const char *channel_name)
{
if (!*channel_name)
return VALIDATION_ERROR_EMPTY;
if (*channel_name != '#' || strpbrk (channel_name, "\7\r\n ,:"))
return VALIDATION_ERROR_INVALID;
if (strlen (channel_name) > IRC_MAX_CHANNEL_NAME)
return VALIDATION_ERROR_TOO_LONG;
return VALIDATION_OK;
}
static bool
irc_is_valid_key (const char *key)
{
// XXX: should be 7-bit as well but whatever
return irc_regex_match ("^[^\r\n\f\t\v ]{1,23}$", key);
}
#undef SN
#undef N4
#undef N6
#undef LE
#undef SP
static bool
irc_is_valid_user_mask (const char *mask)
{
return irc_regex_match ("^[^!@]+![^!@]+@[^@!]+$", mask);
}
static bool
irc_is_valid_fingerprint (const char *fp)
{
return irc_regex_match ("^[a-fA-F0-9]{40}$", fp);
}
// --- Clients (equals users) --------------------------------------------------
#define IRC_SUPPORTED_USER_MODES "aiwros"
enum
{
IRC_USER_MODE_INVISIBLE = (1 << 0),
IRC_USER_MODE_RX_WALLOPS = (1 << 1),
IRC_USER_MODE_RESTRICTED = (1 << 2),
IRC_USER_MODE_OPERATOR = (1 << 3),
IRC_USER_MODE_RX_SERVER_NOTICES = (1 << 4)
};
enum
{
IRC_CAP_MULTI_PREFIX = (1 << 0),
IRC_CAP_INVITE_NOTIFY = (1 << 1),
IRC_CAP_ECHO_MESSAGE = (1 << 2),
IRC_CAP_USERHOST_IN_NAMES = (1 << 3),
IRC_CAP_SERVER_TIME = (1 << 4)
};
struct client
{
LIST_HEADER (struct client)
struct server_context *ctx; ///< Server context
time_t opened; ///< When the connection was opened
size_t n_sent_messages; ///< Number of sent messages total
size_t sent_bytes; ///< Number of sent bytes total
size_t n_received_messages; ///< Number of received messages total
size_t received_bytes; ///< Number of received bytes total
int socket_fd; ///< The TCP socket
struct str read_buffer; ///< Unprocessed input
struct str write_buffer; ///< Output yet to be sent out
struct poller_fd socket_event; ///< The socket can be read/written to
struct poller_timer ping_timer; ///< We should send a ping
struct poller_timer timeout_timer; ///< Connection seems to be dead
struct poller_timer kill_timer; ///< Hard kill timeout
unsigned long cap_version; ///< CAP protocol version
unsigned caps_enabled; ///< Enabled capabilities
unsigned initialized : 1; ///< Has any data been received yet?
unsigned cap_negotiating : 1; ///< Negotiating capabilities
unsigned registered : 1; ///< The user has registered
unsigned closing_link : 1; ///< Closing link
unsigned half_closed : 1; ///< Closing link: conn. is half-closed
unsigned ssl_rx_want_tx : 1; ///< SSL_read() wants to write
unsigned ssl_tx_want_rx : 1; ///< SSL_write() wants to read
SSL *ssl; ///< SSL connection
char *ssl_cert_fingerprint; ///< Client certificate fingerprint
char *nickname; ///< IRC nickname (main identifier)
char *username; ///< IRC username
char *realname; ///< IRC realname (e-mail)
char *hostname; ///< Hostname shown to the network
char *port; ///< Port of the peer as a string
char *address; ///< Full address
unsigned mode; ///< User's mode
char *away_message; ///< Away message
time_t last_active; ///< Last PRIVMSG, to get idle time
struct str_map invites; ///< Channel invitations by operators
struct flood_detector antiflood; ///< Flood detector
struct async_getnameinfo *gni; ///< Backwards DNS resolution
struct poller_timer gni_timer; ///< Backwards DNS resolution timeout
};
static struct client *
client_new (void)
{
struct client *self = xcalloc (1, sizeof *self);
self->socket_fd = -1;
self->read_buffer = str_make ();
self->write_buffer = str_make ();
self->cap_version = 301;
// TODO: make this configurable and more fine-grained
flood_detector_init (&self->antiflood, 10, 20);
self->invites = str_map_make (NULL);
self->invites.key_xfrm = irc_strxfrm;
return self;
}
static void
client_destroy (struct client *self)
{
if (!soft_assert (self->socket_fd == -1))
xclose (self->socket_fd);
if (self->ssl)
SSL_free (self->ssl);
str_free (&self->read_buffer);
str_free (&self->write_buffer);
free (self->ssl_cert_fingerprint);
free (self->nickname);
free (self->username);
free (self->realname);
free (self->hostname);
free (self->port);
free (self->address);
free (self->away_message);
flood_detector_free (&self->antiflood);
str_map_free (&self->invites);
if (self->gni)
async_cancel (&self->gni->async);
free (self);
}
static void client_close_link (struct client *c, const char *reason);
static void client_kill (struct client *c, const char *reason);
static void client_send (struct client *, const char *, ...)
ATTRIBUTE_PRINTF (2, 3);
static void client_cancel_timers (struct client *);
static void client_set_kill_timer (struct client *);
static void client_update_poller (struct client *, const struct pollfd *);
// --- Channels ----------------------------------------------------------------
#define IRC_SUPPORTED_CHAN_MODES "ov" "beI" "imnqpst" "kl"
enum
{
IRC_CHAN_MODE_INVITE_ONLY = (1 << 0),
IRC_CHAN_MODE_MODERATED = (1 << 1),
IRC_CHAN_MODE_NO_OUTSIDE_MSGS = (1 << 2),
IRC_CHAN_MODE_QUIET = (1 << 3),
IRC_CHAN_MODE_PRIVATE = (1 << 4),
IRC_CHAN_MODE_SECRET = (1 << 5),
IRC_CHAN_MODE_PROTECTED_TOPIC = (1 << 6),
IRC_CHAN_MODE_OPERATOR = (1 << 7),
IRC_CHAN_MODE_VOICE = (1 << 8)
};
struct channel_user
{
LIST_HEADER (struct channel_user)
unsigned modes;
struct client *c;
};
struct channel
{
struct server_context *ctx; ///< Server context
char *name; ///< Channel name
unsigned modes; ///< Channel modes
char *key; ///< Channel key
long user_limit; ///< User limit or -1
time_t created; ///< Creation time
char *topic; ///< Channel topic
char *topic_who; ///< Who set the topic
time_t topic_time; ///< When the topic was set
struct channel_user *users; ///< Channel users
struct strv ban_list; ///< Ban list
struct strv exception_list; ///< Exceptions from bans
struct strv invite_list; ///< Exceptions from +I
};
static struct channel *
channel_new (void)
{
struct channel *self = xcalloc (1, sizeof *self);
self->user_limit = -1;
self->topic = xstrdup ("");
self->ban_list = strv_make ();
self->exception_list = strv_make ();
self->invite_list = strv_make ();
return self;
}
static void
channel_delete (struct channel *self)
{
free (self->name);
free (self->key);
free (self->topic);
free (self->topic_who);
struct channel_user *link, *tmp;
for (link = self->users; link; link = tmp)
{
tmp = link->next;
free (link);
}
strv_free (&self->ban_list);
strv_free (&self->exception_list);
strv_free (&self->invite_list);
free (self);
}
static char *
channel_get_mode (struct channel *self, bool disclose_secrets)
{
struct str mode = str_make ();
unsigned m = self->modes;
if (m & IRC_CHAN_MODE_INVITE_ONLY) str_append_c (&mode, 'i');
if (m & IRC_CHAN_MODE_MODERATED) str_append_c (&mode, 'm');
if (m & IRC_CHAN_MODE_NO_OUTSIDE_MSGS) str_append_c (&mode, 'n');
if (m & IRC_CHAN_MODE_QUIET) str_append_c (&mode, 'q');
if (m & IRC_CHAN_MODE_PRIVATE) str_append_c (&mode, 'p');
if (m & IRC_CHAN_MODE_SECRET) str_append_c (&mode, 's');
if (m & IRC_CHAN_MODE_PROTECTED_TOPIC) str_append_c (&mode, 't');
if (self->user_limit != -1) str_append_c (&mode, 'l');
if (self->key) str_append_c (&mode, 'k');
// XXX: is it correct to split it? Try it on an existing implementation.
if (disclose_secrets)
{
if (self->user_limit != -1)
str_append_printf (&mode, " %ld", self->user_limit);
if (self->key)
str_append_printf (&mode, " %s", self->key);
}
return str_steal (&mode);
}
static struct channel_user *
channel_get_user (const struct channel *chan, const struct client *c)
{
for (struct channel_user *iter = chan->users; iter; iter = iter->next)
if (iter->c == c)
return iter;
return NULL;
}
static struct channel_user *
channel_add_user (struct channel *chan, struct client *c)
{
struct channel_user *link = xcalloc (1, sizeof *link);
link->c = c;
LIST_PREPEND (chan->users, link);
return link;
}
static void
channel_remove_user (struct channel *chan, struct channel_user *user)
{
LIST_UNLINK (chan->users, user);
free (user);
}
static size_t
channel_user_count (const struct channel *chan)
{
size_t result = 0;
for (struct channel_user *iter = chan->users; iter; iter = iter->next)
result++;
return result;
}
// --- IRC server context ------------------------------------------------------
struct whowas_info
{
char *nickname; ///< IRC nickname
char *username; ///< IRC username
char *realname; ///< IRC realname
char *hostname; ///< Hostname shown to the network
};
struct whowas_info *
whowas_info_new (struct client *c)
{
struct whowas_info *self = xmalloc (sizeof *self);
self->nickname = xstrdup (c->nickname);
self->username = xstrdup (c->username);
self->realname = xstrdup (c->realname);
self->hostname = xstrdup (c->hostname);
return self;
}
static void
whowas_info_destroy (struct whowas_info *self)
{
free (self->nickname);
free (self->username);
free (self->realname);
free (self->hostname);
free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct irc_command
{
const char *name;
bool requires_registration;
void (*handler) (const struct irc_message *, struct client *);
size_t n_received; ///< Number of commands received
size_t bytes_received; ///< Number of bytes received total
};
struct server_context
{
int *listen_fds; ///< Listening socket FD's
struct poller_fd *listen_events; ///< New connections available
size_t n_listen_fds; ///< Number of listening sockets
time_t started; ///< When has the server been started
SSL_CTX *ssl_ctx; ///< SSL context
struct client *clients; ///< Clients
unsigned n_clients; ///< Current number of connections
struct str_map users; ///< Maps nicknames to clients
struct str_map channels; ///< Maps channel names to data
struct str_map handlers; ///< Message handlers
struct str_map cap_handlers; ///< CAP message handlers
struct str_map whowas; ///< WHOWAS registry
struct poller poller; ///< Manages polled description
struct poller_timer quit_timer; ///< Quit timeout timer
bool quitting; ///< User requested quitting
bool polling; ///< The event loop is running
struct poller_fd signal_event; ///< Got a signal
struct str_map config; ///< Server configuration
char *server_name; ///< Our server name
unsigned ping_interval; ///< Ping interval in seconds
unsigned max_connections; ///< Max. connections allowed or 0
struct strv motd; ///< MOTD (none if empty)
nl_catd catalog; ///< Message catalog for server msgs
struct str_map operators; ///< TLS cert. fingerprints for IRCops
};
static void
on_irc_quit_timeout (void *user_data)
{
struct server_context *ctx = user_data;
struct client *iter, *next;
for (iter = ctx->clients; iter; iter = next)
{
next = iter->next;
// irc_initiate_quit() has already unregistered the client
client_kill (iter, "Shutting down");
}
}
static void
server_context_init (struct server_context *self)
{
memset (self, 0, sizeof *self);
self->users = str_map_make (NULL);
self->users.key_xfrm = irc_strxfrm;
self->channels = str_map_make ((str_map_free_fn) channel_delete);
self->channels.key_xfrm = irc_strxfrm;
self->handlers = str_map_make (NULL);
self->handlers.key_xfrm = irc_strxfrm;
self->cap_handlers = str_map_make (NULL);
self->cap_handlers.key_xfrm = irc_strxfrm;
self->whowas = str_map_make ((str_map_free_fn) whowas_info_destroy);
self->whowas.key_xfrm = irc_strxfrm;
poller_init (&self->poller);
self->quit_timer = poller_timer_make (&self->poller);
self->quit_timer.dispatcher = on_irc_quit_timeout;
self->quit_timer.user_data = self;
self->config = str_map_make (free);
simple_config_load_defaults (&self->config, g_config_table);
self->motd = strv_make ();
self->catalog = (nl_catd) -1;
self->operators = str_map_make (NULL);
// The regular irc_strxfrm() is sufficient for fingerprints
self->operators.key_xfrm = irc_strxfrm;
}
static void
server_context_free (struct server_context *self)
{
str_map_free (&self->config);
for (size_t i = 0; i < self->n_listen_fds; i++)
{
poller_fd_reset (&self->listen_events[i]);
xclose (self->listen_fds[i]);
}
free (self->listen_fds);
free (self->listen_events);
hard_assert (!self->clients);
if (self->ssl_ctx)
SSL_CTX_free (self->ssl_ctx);
free (self->server_name);
str_map_free (&self->users);
str_map_free (&self->channels);
str_map_free (&self->handlers);
str_map_free (&self->cap_handlers);
str_map_free (&self->whowas);
poller_free (&self->poller);
strv_free (&self->motd);
if (self->catalog != (nl_catd) -1)
catclose (self->catalog);
str_map_free (&self->operators);
}
static const char *
irc_get_text (struct server_context *ctx, int id, const char *def)
{
if (!soft_assert (def != NULL))
def = "";
if (ctx->catalog == (nl_catd) -1)
return def;
return catgets (ctx->catalog, 1, id, def);
}
static void
irc_try_finish_quit (struct server_context *ctx)
{
if (!ctx->n_clients && ctx->quitting)
{
poller_timer_reset (&ctx->quit_timer);
ctx->polling = false;
}
}
static void
irc_initiate_quit (struct server_context *ctx)
{
print_status ("shutting down");
for (size_t i = 0; i < ctx->n_listen_fds; i++)
{
poller_fd_reset (&ctx->listen_events[i]);
xclose (ctx->listen_fds[i]);
}
ctx->n_listen_fds = 0;
for (struct client *iter = ctx->clients; iter; iter = iter->next)
if (!iter->closing_link)
client_close_link (iter, "Shutting down");
ctx->quitting = true;
poller_timer_set (&ctx->quit_timer, 5000);
irc_try_finish_quit (ctx);
}
static struct channel *
irc_channel_create (struct server_context *ctx, const char *name)
{
struct channel *chan = channel_new ();
chan->ctx = ctx;
chan->name = xstrdup (name);
chan->created = time (NULL);
str_map_set (&ctx->channels, name, chan);
return chan;
}
static void
irc_channel_destroy_if_empty (struct server_context *ctx, struct channel *chan)
{
if (!chan->users)
str_map_set (&ctx->channels, chan->name, NULL);
}
static void
irc_send_to_roommates (struct client *c, const char *message)
{
struct str_map targets = str_map_make (NULL);
targets.key_xfrm = irc_strxfrm;
struct str_map_iter iter = str_map_iter_make (&c->ctx->channels);
struct channel *chan;
while ((chan = str_map_iter_next (&iter)))
{
if (chan->modes & IRC_CHAN_MODE_QUIET
|| !channel_get_user (chan, c))
continue;
for (struct channel_user *iter = chan->users; iter; iter = iter->next)
str_map_set (&targets, iter->c->nickname, iter->c);
}
iter = str_map_iter_make (&targets);
struct client *target;
while ((target = str_map_iter_next (&iter)))
if (target != c)
client_send (target, "%s", message);
str_map_free (&targets);
}
// --- Clients (continued) -----------------------------------------------------
static void
client_mode_to_str (unsigned m, struct str *out)
{
if (m & IRC_USER_MODE_INVISIBLE) str_append_c (out, 'i');
if (m & IRC_USER_MODE_RX_WALLOPS) str_append_c (out, 'w');
if (m & IRC_USER_MODE_RESTRICTED) str_append_c (out, 'r');
if (m & IRC_USER_MODE_OPERATOR) str_append_c (out, 'o');
if (m & IRC_USER_MODE_RX_SERVER_NOTICES) str_append_c (out, 's');
}
static char *
client_get_mode (struct client *self)
{
struct str mode = str_make ();
if (self->away_message)
str_append_c (&mode, 'a');
client_mode_to_str (self->mode, &mode);
return str_steal (&mode);
}
static void
client_send_str (struct client *c, const struct str *s)
{
hard_assert (!c->closing_link);
size_t old_sendq = c->write_buffer.len;
// So far there's only one message tag we use, so we can do it simple;
// note that a 1024-character limit applies to messages with tags on
if (c->caps_enabled & IRC_CAP_SERVER_TIME)
{
long milliseconds; char buf[32]; struct tm tm;
time_t now = unixtime_msec (&milliseconds);
if (soft_assert (strftime (buf, sizeof buf,
"%Y-%m-%dT%T", gmtime_r (&now, &tm))))
str_append_printf (&c->write_buffer,
"@time=%s.%03ldZ ", buf, milliseconds);
}
// TODO: kill the connection above some "SendQ" threshold (careful!)
str_append_data (&c->write_buffer, s->str,
MIN (s->len, IRC_MAX_MESSAGE_LENGTH));
str_append (&c->write_buffer, "\r\n");
// XXX: we might want to move this elsewhere, so that it doesn't get called
// as often; it's going to cause a lot of syscalls with epoll.
client_update_poller (c, NULL);
// Technically we haven't sent it yet but that's a minor detail
c->n_sent_messages++;
c->sent_bytes += c->write_buffer.len - old_sendq;
}
static void
client_send (struct client *c, const char *format, ...)
{
struct str tmp = str_make ();
va_list ap;
va_start (ap, format);
str_append_vprintf (&tmp, format, ap);
va_end (ap);
client_send_str (c, &tmp);
str_free (&tmp);
}
static void
client_add_to_whowas (struct client *c)
{
// Only keeping one entry for each nickname
// TODO: make sure this list doesn't get too long, for example by
// putting them in a linked list ordered by time
str_map_set (&c->ctx->whowas, c->nickname, whowas_info_new (c));
}
static void
client_unregister (struct client *c, const char *reason)
{
if (!c->registered)
return;
char *message = xstrdup_printf (":%s!%s@%s QUIT :%s",
c->nickname, c->username, c->hostname, reason);
irc_send_to_roommates (c, message);
free (message);
struct str_map_unset_iter iter =
str_map_unset_iter_make (&c->ctx->channels);
struct channel *chan;
while ((chan = str_map_unset_iter_next (&iter)))
{
struct channel_user *user;
if (!(user = channel_get_user (chan, c)))
continue;
channel_remove_user (chan, user);
irc_channel_destroy_if_empty (c->ctx, chan);
}
str_map_unset_iter_free (&iter);
client_add_to_whowas (c);
str_map_set (&c->ctx->users, c->nickname, NULL);
cstr_set (&c->nickname, NULL);
c->registered = false;
}
static void
client_kill (struct client *c, const char *reason)
{
struct server_context *ctx = c->ctx;
client_unregister (c, reason ? reason : "Client exited");
if (c->address)
// Only log the event if address resolution has finished
print_debug ("closed connection to %s (%s)", c->address,
reason ? reason : "");
if (c->ssl)
// Note that we might have already called this once, but that is fine
(void) SSL_shutdown (c->ssl);
xclose (c->socket_fd);
c->socket_fd = -1;
// We don't fork any children, this is okay
c->socket_event.closed = true;
poller_fd_reset (&c->socket_event);
client_cancel_timers (c);
LIST_UNLINK (ctx->clients, c);
ctx->n_clients--;
client_destroy (c);
irc_try_finish_quit (ctx);
}
static void
client_close_link (struct client *c, const char *reason)
{
// Let's just cut the connection, the client can try again later.
// We also want to avoid accidentally setting poller events before
// address resolution has finished.
if (!c->initialized)
{
client_kill (c, reason);
return;
}
if (!soft_assert (!c->closing_link))
return;
// We push an `ERROR' message to the write buffer and let the poller send
// it, with some arbitrary timeout. The `closing_link' state makes sure
// that a/ we ignore any successive messages, and b/ that the connection
// is killed after the write buffer is transferred and emptied.
client_send (c, "ERROR :Closing Link: %s[%s] (%s)",
c->nickname ? c->nickname : "*",
c->hostname /* TODO host IP? */, reason);
c->closing_link = true;
client_unregister (c, reason);
client_set_kill_timer (c);
}
static bool
client_in_mask_list (const struct client *c, const struct strv *mask)
{
char *client = xstrdup_printf ("%s!%s@%s",
c->nickname, c->username, c->hostname);
bool result = false;
for (size_t i = 0; i < mask->len; i++)
if (!irc_fnmatch (mask->vector[i], client))
{
result = true;
break;
}
free (client);
return result;
}
static char *
client_get_ssl_cert_fingerprint (struct client *c)
{
if (!c->ssl)
return NULL;
X509 *peer_cert = SSL_get_peer_certificate (c->ssl);
if (!peer_cert)
return NULL;
int cert_len = i2d_X509 (peer_cert, NULL);
if (cert_len < 0)
return NULL;
unsigned char cert[cert_len], *p = cert;
if (i2d_X509 (peer_cert, &p) < 0)
return NULL;
unsigned char hash[SHA_DIGEST_LENGTH];
SHA1 (cert, cert_len, hash);
struct str fingerprint = str_make ();
for (size_t i = 0; i < sizeof hash; i++)
str_append_printf (&fingerprint, "%02x", hash[i]);
return str_steal (&fingerprint);
}
// --- Timers ------------------------------------------------------------------
static void
client_cancel_timers (struct client *c)
{
poller_timer_reset (&c->kill_timer);
poller_timer_reset (&c->timeout_timer);
poller_timer_reset (&c->ping_timer);
poller_timer_reset (&c->gni_timer);
}
static void
client_set_timer (struct client *c,
struct poller_timer *timer, unsigned interval)
{
client_cancel_timers (c);
poller_timer_set (timer, interval * 1000);
}
static void
on_client_kill_timer (struct client *c)
{
hard_assert (!c->initialized || c->closing_link);
client_kill (c, NULL);
}
static void
client_set_kill_timer (struct client *c)
{
client_set_timer (c, &c->kill_timer, c->ctx->ping_interval);
}
static void
on_client_timeout_timer (struct client *c)
{
char *reason = xstrdup_printf
("Ping timeout: >%u seconds", c->ctx->ping_interval);
client_close_link (c, reason);
free (reason);
}
static void
on_client_ping_timer (struct client *c)
{
hard_assert (!c->closing_link);
client_send (c, "PING :%s", c->ctx->server_name);
client_set_timer (c, &c->timeout_timer, c->ctx->ping_interval);
}
static void
client_set_ping_timer (struct client *c)
{
client_set_timer (c, &c->ping_timer, c->ctx->ping_interval);
}
// --- IRC command handling ----------------------------------------------------
static void
irc_make_reply (struct client *c, int id, va_list ap, struct str *output)
{
str_append_printf (output, ":%s %03d %s ",
c->ctx->server_name, id, c->nickname ? c->nickname : "*");
str_append_vprintf (output,
irc_get_text (c->ctx, id, g_default_replies[id]), ap);
}
// XXX: this way we cannot typecheck the arguments, so we must be careful
static void
irc_send_reply (struct client *c, int id, ...)
{
struct str reply = str_make ();
va_list ap;
va_start (ap, id);
irc_make_reply (c, id, ap, &reply);
va_end (ap);
client_send_str (c, &reply);
str_free (&reply);
}
/// Send a space-separated list of words across as many replies as needed
static void
irc_send_reply_vector (struct client *c, int id, char **items, ...)
{
struct str common = str_make ();
va_list ap;
va_start (ap, items);
irc_make_reply (c, id, ap, &common);
va_end (ap);
// We always send at least one message (there might be a client that
// expects us to send this message at least once)
do
{
struct str reply = str_make ();
str_append_str (&reply, &common);
// If not even a single item fits in the limit (which may happen,
// in theory) it just gets cropped. We could also skip it.
if (*items)
str_append (&reply, *items++);
// Append as many items as fits in a single message
while (*items
&& reply.len + 1 + strlen (*items) <= IRC_MAX_MESSAGE_LENGTH)
str_append_printf (&reply, " %s", *items++);
client_send_str (c, &reply);
str_free (&reply);
}
while (*items);
str_free (&common);
}
#define RETURN_WITH_REPLY(c, ...) \
BLOCK_START \
irc_send_reply ((c), __VA_ARGS__); \
return; \
BLOCK_END
static void
irc_send_motd (struct client *c)
{
struct server_context *ctx = c->ctx;
if (!ctx->motd.len)
RETURN_WITH_REPLY (c, IRC_ERR_NOMOTD);
irc_send_reply (c, IRC_RPL_MOTDSTART, ctx->server_name);
for (size_t i = 0; i < ctx->motd.len; i++)
irc_send_reply (c, IRC_RPL_MOTD, ctx->motd.vector[i]);
irc_send_reply (c, IRC_RPL_ENDOFMOTD);
}
static void
irc_send_lusers (struct client *c)
{
int n_users = 0, n_services = 0, n_opers = 0, n_unknown = 0;
for (struct client *link = c->ctx->clients; link; link = link->next)
{
if (link->registered)
n_users++;
else
n_unknown++;
if (link->mode & IRC_USER_MODE_OPERATOR)
n_opers++;
}
int n_channels = 0;
struct str_map_iter iter = str_map_iter_make (&c->ctx->channels);
struct channel *chan;
while ((chan = str_map_iter_next (&iter)))
if (!(chan->modes & IRC_CHAN_MODE_SECRET)
|| channel_get_user (chan, c))
n_channels++;
irc_send_reply (c, IRC_RPL_LUSERCLIENT,
n_users, n_services, 1 /* servers total */);
if (n_opers)
irc_send_reply (c, IRC_RPL_LUSEROP, n_opers);
if (n_unknown)
irc_send_reply (c, IRC_RPL_LUSERUNKNOWN, n_unknown);
if (n_channels)
irc_send_reply (c, IRC_RPL_LUSERCHANNELS, n_channels);
irc_send_reply (c, IRC_RPL_LUSERME,
n_users + n_services + n_unknown, 0 /* peer servers */);
}
static bool
irc_is_this_me (struct server_context *ctx, const char *target)
{
// Target servers can also be matched by their users
return !irc_fnmatch (target, ctx->server_name)
|| str_map_find (&ctx->users, target);
}
static void
irc_send_isupport (struct client *c)
{
// Only # channels, +e supported, +I supported, unlimited arguments to MODE
irc_send_reply (c, IRC_RPL_ISUPPORT, "CHANTYPES=# EXCEPTS INVEX MODES"
" TARGMAX=WHOIS:,LIST:,NAMES:,PRIVMSG:1,NOTICE:1,KICK:"
" NICKLEN=" XSTRINGIFY (IRC_MAX_NICKNAME)
" CHANNELLEN=" XSTRINGIFY (IRC_MAX_CHANNEL_NAME));
}
static void
irc_try_finish_registration (struct client *c)
{
struct server_context *ctx = c->ctx;
if (!c->nickname || !c->username || !c->realname)
return;
if (c->registered || c->cap_negotiating)
return;
c->registered = true;
irc_send_reply (c, IRC_RPL_WELCOME, c->nickname, c->username, c->hostname);
irc_send_reply (c, IRC_RPL_YOURHOST, ctx->server_name, PROGRAM_VERSION);
// The purpose of this message eludes me
irc_send_reply (c, IRC_RPL_CREATED, __DATE__);
irc_send_reply (c, IRC_RPL_MYINFO, ctx->server_name, PROGRAM_VERSION,
IRC_SUPPORTED_USER_MODES, IRC_SUPPORTED_CHAN_MODES);
irc_send_isupport (c);
irc_send_lusers (c);
irc_send_motd (c);
char *mode = client_get_mode (c);
if (*mode)
client_send (c, ":%s MODE %s :+%s", c->nickname, c->nickname, mode);
free (mode);
hard_assert (c->ssl_cert_fingerprint == NULL);
if ((c->ssl_cert_fingerprint = client_get_ssl_cert_fingerprint (c)))
client_send (c, ":%s NOTICE %s :"
"Your TLS client certificate fingerprint is %s",
ctx->server_name, c->nickname, c->ssl_cert_fingerprint);
str_map_set (&ctx->whowas, c->nickname, NULL);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// IRCv3 capability negotiation. See http://ircv3.org for details.
struct irc_cap_args
{
const char *subcommand; ///< The subcommand being processed
const char *full_params; ///< Whole parameter string
struct strv params; ///< Split parameters
const char *target; ///< Target parameter for replies
};
static struct
{
unsigned flag; ///< Flag
const char *name; ///< Name of the capability
}
irc_cap_table[] =
{
{ IRC_CAP_MULTI_PREFIX, "multi-prefix" },
{ IRC_CAP_INVITE_NOTIFY, "invite-notify" },
{ IRC_CAP_ECHO_MESSAGE, "echo-message" },
{ IRC_CAP_USERHOST_IN_NAMES, "userhost-in-names" },
{ IRC_CAP_SERVER_TIME, "server-time" },
};
static void
irc_handle_cap_ls (struct client *c, struct irc_cap_args *a)
{
if (a->params.len == 1
&& !xstrtoul (&c->cap_version, a->params.vector[0], 10))
irc_send_reply (c, IRC_ERR_INVALIDCAPCMD,
a->subcommand, "Ignoring invalid protocol version number");
c->cap_negotiating = true;
client_send (c, ":%s CAP %s LS :multi-prefix invite-notify echo-message"
" userhost-in-names server-time", c->ctx->server_name, a->target);
}
static void
irc_handle_cap_list (struct client *c, struct irc_cap_args *a)
{
struct strv caps = strv_make ();
for (size_t i = 0; i < N_ELEMENTS (irc_cap_table); i++)
if (c->caps_enabled & irc_cap_table[i].flag)
strv_append (&caps, irc_cap_table[i].name);
char *caps_str = strv_join (&caps, " ");
strv_free (&caps);
client_send (c, ":%s CAP %s LIST :%s",
c->ctx->server_name, a->target, caps_str);
free (caps_str);
}
static unsigned
irc_decode_capability (const char *name)
{
for (size_t i = 0; i < N_ELEMENTS (irc_cap_table); i++)
if (!strcmp (irc_cap_table[i].name, name))
return irc_cap_table[i].flag;
return 0;
}
static void
irc_handle_cap_req (struct client *c, struct irc_cap_args *a)
{
c->cap_negotiating = true;
unsigned new_caps = c->caps_enabled;
bool success = true;
for (size_t i = 0; i < a->params.len; i++)
{
bool removing = false;
const char *name = a->params.vector[i];
if (*name == '-')
{
removing = true;
name++;
}
unsigned cap;
if (!(cap = irc_decode_capability (name)))
success = false;
else if (removing)
new_caps &= ~cap;
else
new_caps |= cap;
}
if (success)
{
c->caps_enabled = new_caps;
client_send (c, ":%s CAP %s ACK :%s",
c->ctx->server_name, a->target, a->full_params);
}
else
client_send (c, ":%s CAP %s NAK :%s",
c->ctx->server_name, a->target, a->full_params);
}
static void
irc_handle_cap_ack (struct client *c, struct irc_cap_args *a)
{
if (a->params.len)
irc_send_reply (c, IRC_ERR_INVALIDCAPCMD,
a->subcommand, "No acknowledgable capabilities supported");
}
static void
irc_handle_cap_end (struct client *c, struct irc_cap_args *a)
{
(void) a;
c->cap_negotiating = false;
irc_try_finish_registration (c);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct irc_cap_command
{
const char *name;
void (*handler) (struct client *, struct irc_cap_args *);
};
static void
irc_register_cap_handlers (struct server_context *ctx)
{
static const struct irc_cap_command cap_handlers[] =
{
{ "LS", irc_handle_cap_ls },
{ "LIST", irc_handle_cap_list },
{ "REQ", irc_handle_cap_req },
{ "ACK", irc_handle_cap_ack },
{ "END", irc_handle_cap_end },
};
for (size_t i = 0; i < N_ELEMENTS (cap_handlers); i++)
{
const struct irc_cap_command *cmd = &cap_handlers[i];
str_map_set (&ctx->cap_handlers, cmd->name, (void *) cmd);
}
}
static void
irc_handle_cap (const struct irc_message *msg, struct client *c)
{
if (msg->params.len < 1)
RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
struct irc_cap_args args;
args.target = c->nickname ? c->nickname : "*";
args.subcommand = msg->params.vector[0];
args.full_params = "";
args.params = strv_make ();
if (msg->params.len > 1)
{
args.full_params = msg->params.vector[1];
cstr_split (args.full_params, " ", true, &args.params);
}
struct irc_cap_command *cmd =
str_map_find (&c->ctx->cap_handlers, args.subcommand);
if (!cmd)
irc_send_reply (c, IRC_ERR_INVALIDCAPCMD,
args.subcommand, "Invalid CAP subcommand");
else
cmd->handler (c, &args);
strv_free (&args.params);
}
static void
irc_handle_pass (const struct irc_message *msg, struct client *c)
{
if (c->registered)
irc_send_reply (c, IRC_ERR_ALREADYREGISTERED);
else if (msg->params.len < 1)
irc_send_reply (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
// We have TLS client certificates for this purpose; ignoring
}
static void
irc_handle_nick (const struct irc_message *msg, struct client *c)
{
struct server_context *ctx = c->ctx;
if (msg->params.len < 1)
RETURN_WITH_REPLY (c, IRC_ERR_NONICKNAMEGIVEN);
const char *nickname = msg->params.vector[0];
if (irc_validate_nickname (nickname) != VALIDATION_OK)
RETURN_WITH_REPLY (c, IRC_ERR_ERRONEOUSNICKNAME, nickname);
struct client *client = str_map_find (&ctx->users, nickname);
if (client && client != c)
RETURN_WITH_REPLY (c, IRC_ERR_NICKNAMEINUSE, nickname);
// Nothing to do here, let's not annoy roommates
if (c->nickname && !strcmp (c->nickname, nickname))
return;
if (c->registered)
{
client_add_to_whowas (c);
char *message = xstrdup_printf (":%s!%s@%s NICK :%s",
c->nickname, c->username, c->hostname, nickname);
irc_send_to_roommates (c, message);
client_send (c, "%s", message);
free (message);
}
// Release the old nickname and allocate a new one
if (c->nickname)
str_map_set (&ctx->users, c->nickname, NULL);
cstr_set (&c->nickname, xstrdup (nickname));
str_map_set (&ctx->users, nickname, c);
irc_try_finish_registration (c);
}
static void
irc_handle_user (const struct irc_message *msg, struct client *c)
{
if (c->registered)
RETURN_WITH_REPLY (c, IRC_ERR_ALREADYREGISTERED);
if (msg->params.len < 4)
RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
const char *username = msg->params.vector[0];
const char *mode = msg->params.vector[1];
const char *realname = msg->params.vector[3];
// Unfortunately the protocol doesn't give us any means of rejecting it
if (!irc_is_valid_user (username))
username = "xxx";
cstr_set (&c->username, xstrdup (username));
cstr_set (&c->realname, xstrdup (realname));
c->mode = 0;
unsigned long m;
if (xstrtoul (&m, mode, 10))
{
if (m & 4) c->mode |= IRC_USER_MODE_RX_WALLOPS;
if (m & 8) c->mode |= IRC_USER_MODE_INVISIBLE;
}
irc_try_finish_registration (c);
}
static void
irc_handle_userhost (const struct irc_message *msg, struct client *c)
{
if (msg->params.len < 1)
RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
struct str reply = str_make ();
for (size_t i = 0; i < 5 && i < msg->params.len; i++)
{
const char *nick = msg->params.vector[i];
struct client *target = str_map_find (&c->ctx->users, nick);
if (!target)
continue;
if (i)
str_append_c (&reply, ' ');
str_append (&reply, nick);
if (target->mode & IRC_USER_MODE_OPERATOR)
str_append_c (&reply, '*');
str_append_printf (&reply, "=%c%s@%s",
target->away_message ? '-' : '+',
target->username, target->hostname);
}
irc_send_reply (c, IRC_RPL_USERHOST, reply.str);
str_free (&reply);
}
static void
irc_handle_lusers (const struct irc_message *msg, struct client *c)
{
if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[1]))
irc_send_reply (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[1]);
else
irc_send_lusers (c);
}
static void
irc_handle_motd (const struct irc_message *msg, struct client *c)
{
if (msg->params.len > 0 && !irc_is_this_me (c->ctx, msg->params.vector[0]))
irc_send_reply (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[0]);
else
irc_send_motd (c);
}
static void
irc_handle_ping (const struct irc_message *msg, struct client *c)
{
// XXX: the RFC is pretty incomprehensible about the exact usage
if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[1]))
irc_send_reply (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[1]);
else if (msg->params.len < 1)
irc_send_reply (c, IRC_ERR_NOORIGIN);
else
client_send (c, ":%s PONG :%s",
c->ctx->server_name, msg->params.vector[0]);
}
static void
irc_handle_pong (const struct irc_message *msg, struct client *c)
{
// We are the only server, so we don't have to care too much
if (msg->params.len < 1)
irc_send_reply (c, IRC_ERR_NOORIGIN);
else
// Set a new timer to send another PING
client_set_ping_timer (c);
}
static void
irc_handle_quit (const struct irc_message *msg, struct client *c)
{
char *reason = xstrdup_printf ("Quit: %s",
msg->params.len > 0 ? msg->params.vector[0] : c->nickname);
client_close_link (c, reason);
free (reason);
}
static void
irc_handle_time (const struct irc_message *msg, struct client *c)
{
if (msg->params.len > 0 && !irc_is_this_me (c->ctx, msg->params.vector[0]))
RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[0]);
char buf[32] = "";
time_t now = time (NULL);
struct tm tm;
strftime (buf, sizeof buf, "%a %b %d %Y %T", localtime_r (&now, &tm));
irc_send_reply (c, IRC_RPL_TIME, c->ctx->server_name, buf);
}
static void
irc_handle_version (const struct irc_message *msg, struct client *c)
{
if (msg->params.len > 0 && !irc_is_this_me (c->ctx, msg->params.vector[0]))
RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[0]);
irc_send_reply (c, IRC_RPL_VERSION, PROGRAM_VERSION, g_debug_mode,
c->ctx->server_name, PROGRAM_NAME " " PROGRAM_VERSION);
irc_send_isupport (c);
}
static void
irc_channel_multicast (struct channel *chan, const char *message,
struct client *except)
{
for (struct channel_user *iter = chan->users; iter; iter = iter->next)
if (iter->c != except)
client_send (iter->c, "%s", message);
}
static bool
irc_modify_mode (unsigned *mask, unsigned mode, bool add)
{
unsigned orig = *mask;
if (add)
*mask |= mode;
else
*mask &= ~mode;
return *mask != orig;
}
static void
irc_update_user_mode (struct client *c, unsigned new_mode)
{
unsigned old_mode = c->mode;
c->mode = new_mode;
unsigned added = new_mode & ~old_mode;
unsigned removed = old_mode & ~new_mode;
struct str diff = str_make ();
if (added)
{
str_append_c (&diff, '+');
client_mode_to_str (added, &diff);
}
if (removed)
{
str_append_c (&diff, '-');
client_mode_to_str (removed, &diff);
}
if (diff.len)
client_send (c, ":%s MODE %s :%s",
c->nickname, c->nickname, diff.str);
str_free (&diff);
}
static void
irc_handle_user_mode_change (struct client *c, const char *mode_string)
{
unsigned new_mode = c->mode;
bool adding = true;
while (*mode_string)
switch (*mode_string++)
{
case '+': adding = true; break;
case '-': adding = false; break;
case 'a':
// Ignore, the client should use AWAY
break;
case 'i':
irc_modify_mode (&new_mode, IRC_USER_MODE_INVISIBLE, adding);
break;
case 'w':
irc_modify_mode (&new_mode, IRC_USER_MODE_RX_WALLOPS, adding);
break;
case 'r':
// It's not possible to un-restrict yourself
if (adding)
new_mode |= IRC_USER_MODE_RESTRICTED;
break;
case 'o':
if (!adding)
new_mode &= ~IRC_USER_MODE_OPERATOR;
else if (c->ssl_cert_fingerprint
&& str_map_find (&c->ctx->operators, c->ssl_cert_fingerprint))
new_mode |= IRC_USER_MODE_OPERATOR;
else
client_send (c, ":%s NOTICE %s :Either you're not using an TLS"
" client certificate, or the fingerprint doesn't match",
c->ctx->server_name, c->nickname);
break;
case 's':
irc_modify_mode (&new_mode, IRC_USER_MODE_RX_SERVER_NOTICES, adding);
break;
default:
RETURN_WITH_REPLY (c, IRC_ERR_UMODEUNKNOWNFLAG);
}
irc_update_user_mode (c, new_mode);
}
static void
irc_send_channel_list (struct client *c, const char *channel_name,
const struct strv *list, int reply, int end_reply)
{
for (size_t i = 0; i < list->len; i++)
irc_send_reply (c, reply, channel_name, list->vector[i]);
irc_send_reply (c, end_reply, channel_name);
}
static char *
irc_check_expand_user_mask (const char *mask)
{
struct str result = str_make ();
str_append (&result, mask);
// Make sure it is a complete mask
if (!strchr (result.str, '!'))
str_append (&result, "!*");
if (!strchr (result.str, '@'))
str_append (&result, "@*");
// And validate whatever the result is
if (!irc_is_valid_user_mask (result.str))
{
str_free (&result);
return NULL;
}
return str_steal (&result);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Channel MODE command handling. This is by far the worst command to implement
// from the whole RFC; don't blame me if it doesn't work exactly as expected.
struct mode_processor
{
// Inputs to set after initialization:
char **params; ///< Mode string parameters
struct client *c; ///< Who does the changes
struct channel *channel; ///< The channel we're modifying
struct channel_user *user; ///< Presence of the client in the chan
// Internals:
bool adding; ///< Currently adding modes
char mode_char; ///< Currently processed mode char
struct str added; ///< Added modes
struct str removed; ///< Removed modes
struct strv added_params; ///< Params for added modes
struct strv removed_params; ///< Params for removed modes
struct str *output; ///< "added" or "removed"
struct strv *output_params; ///< Similarly for "*_params"
};
static struct mode_processor
mode_processor_make (void)
{
return (struct mode_processor)
{
.added = str_make (), .added_params = strv_make (),
.removed = str_make (), .removed_params = strv_make (),
};
}
static void
mode_processor_free (struct mode_processor *self)
{
str_free (&self->added);
str_free (&self->removed);
strv_free (&self->added_params);
strv_free (&self->removed_params);
}
static const char *
mode_processor_next_param (struct mode_processor *self)
{
if (!*self->params)
return NULL;
return *self->params++;
}
static bool
mode_processor_check_operator (struct mode_processor *self)
{
if ((self->user && (self->user->modes & IRC_CHAN_MODE_OPERATOR))
|| (self->c->mode & IRC_USER_MODE_OPERATOR))
return true;
irc_send_reply (self->c, IRC_ERR_CHANOPRIVSNEEDED, self->channel->name);
return false;
}
static void
mode_processor_do_user (struct mode_processor *self, int mode)
{
const char *target = mode_processor_next_param (self);
if (!mode_processor_check_operator (self) || !target)
return;
struct client *client;
struct channel_user *target_user;
if (!(client = str_map_find (&self->c->ctx->users, target)))
irc_send_reply (self->c, IRC_ERR_NOSUCHNICK, target);
else if (!(target_user = channel_get_user (self->channel, client)))
irc_send_reply (self->c, IRC_ERR_USERNOTINCHANNEL,
target, self->channel->name);
else if (irc_modify_mode (&target_user->modes, mode, self->adding))
{
str_append_c (self->output, self->mode_char);
strv_append (self->output_params, client->nickname);
}
}
static bool
mode_processor_do_chan (struct mode_processor *self, int mode)
{
if (!mode_processor_check_operator (self)
|| !irc_modify_mode (&self->channel->modes, mode, self->adding))
return false;
str_append_c (self->output, self->mode_char);
return true;
}
static void
mode_processor_do_chan_remove
(struct mode_processor *self, char mode_char, int mode)
{
if (self->adding
&& irc_modify_mode (&self->channel->modes, mode, false))
str_append_c (&self->removed, mode_char);
}
static void
mode_processor_do_list (struct mode_processor *self,
struct strv *list, int list_msg, int end_msg)
{
const char *target = mode_processor_next_param (self);
if (!target)
{
if (self->adding)
irc_send_channel_list (self->c, self->channel->name,
list, list_msg, end_msg);
return;
}
if (!mode_processor_check_operator (self))
return;
char *mask = irc_check_expand_user_mask (target);
if (!mask)
return;
size_t i;
for (i = 0; i < list->len; i++)
if (!irc_strcmp (list->vector[i], mask))
break;
bool found = i != list->len;
if (found != self->adding)
{
if (self->adding)
strv_append (list, mask);
else
strv_remove (list, i);
str_append_c (self->output, self->mode_char);
strv_append (self->output_params, mask);
}
free (mask);
}
static void
mode_processor_do_key (struct mode_processor *self)
{
const char *target = mode_processor_next_param (self);
if (!mode_processor_check_operator (self) || !target)
return;
if (!self->adding)
{
if (!self->channel->key || irc_strcmp (target, self->channel->key))
return;
str_append_c (&self->removed, self->mode_char);
strv_append (&self->removed_params, self->channel->key);
cstr_set (&self->channel->key, NULL);
}
else if (!irc_is_valid_key (target))
// TODO: we should notify the user somehow
return;
else if (self->channel->key)
irc_send_reply (self->c, IRC_ERR_KEYSET, self->channel->name);
else
{
self->channel->key = xstrdup (target);
str_append_c (&self->added, self->mode_char);
strv_append (&self->added_params, self->channel->key);
}
}
static void
mode_processor_do_limit (struct mode_processor *self)
{
if (!mode_processor_check_operator (self))
return;
const char *target;
if (!self->adding)
{
if (self->channel->user_limit == -1)
return;
self->channel->user_limit = -1;
str_append_c (&self->removed, self->mode_char);
}
else if ((target = mode_processor_next_param (self)))
{
unsigned long x;
if (xstrtoul (&x, target, 10) && x > 0 && x <= LONG_MAX)
{
self->channel->user_limit = x;
str_append_c (&self->added, self->mode_char);
strv_append (&self->added_params, target);
}
}
}
static bool
mode_processor_step (struct mode_processor *self, char mode_char)
{
switch ((self->mode_char = mode_char))
{
case '+':
self->adding = true;
self->output = &self->added;
self->output_params = &self->added_params;
break;
case '-':
self->adding = false;
self->output = &self->removed;
self->output_params = &self->removed_params;
break;
#define USER(mode) mode_processor_do_user (self, (mode))
#define CHAN(mode) mode_processor_do_chan (self, (mode))
case 'o': USER (IRC_CHAN_MODE_OPERATOR); break;
case 'v': USER (IRC_CHAN_MODE_VOICE); break;
case 'i': CHAN (IRC_CHAN_MODE_INVITE_ONLY); break;
case 'm': CHAN (IRC_CHAN_MODE_MODERATED); break;
case 'n': CHAN (IRC_CHAN_MODE_NO_OUTSIDE_MSGS); break;
case 'q': CHAN (IRC_CHAN_MODE_QUIET); break;
case 't': CHAN (IRC_CHAN_MODE_PROTECTED_TOPIC); break;
case 'p':
if (CHAN (IRC_CHAN_MODE_PRIVATE))
mode_processor_do_chan_remove (self, 's', IRC_CHAN_MODE_SECRET);
break;
case 's':
if (CHAN (IRC_CHAN_MODE_SECRET))
mode_processor_do_chan_remove (self, 'p', IRC_CHAN_MODE_PRIVATE);
break;
#undef USER
#undef CHAN
case 'b':
mode_processor_do_list (self, &self->channel->ban_list,
IRC_RPL_BANLIST, IRC_RPL_ENDOFBANLIST);
break;
case 'e':
mode_processor_do_list (self, &self->channel->exception_list,
IRC_RPL_EXCEPTLIST, IRC_RPL_ENDOFEXCEPTLIST);
break;
case 'I':
mode_processor_do_list (self, &self->channel->invite_list,
IRC_RPL_INVITELIST, IRC_RPL_ENDOFINVITELIST);
break;
case 'k':
mode_processor_do_key (self);
break;
case 'l':
mode_processor_do_limit (self);
break;
default:
// It's not safe to continue, results could be undesired
irc_send_reply (self->c, IRC_ERR_UNKNOWNMODE,
mode_char, self->channel->name);
return false;
}
return true;
}
static void
irc_handle_chan_mode_change
(struct client *c, struct channel *chan, char *params[])
{
struct mode_processor p = mode_processor_make ();
p.params = params;
p.channel = chan;
p.c = c;
p.user = channel_get_user (chan, c);
const char *mode_string;
while ((mode_string = mode_processor_next_param (&p)))
{
mode_processor_step (&p, '+');
while (*mode_string)
if (!mode_processor_step (&p, *mode_string++))
goto done_processing;
}
// TODO: limit to three changes with parameter per command
done_processing:
if (p.added.len || p.removed.len)
{
struct str message = str_make ();
str_append_printf (&message, ":%s!%s@%s MODE %s ",
p.c->nickname, p.c->username, p.c->hostname,
p.channel->name);
if (p.added.len)
str_append_printf (&message, "+%s", p.added.str);
if (p.removed.len)
str_append_printf (&message, "-%s", p.removed.str);
for (size_t i = 0; i < p.added_params.len; i++)
str_append_printf (&message, " %s", p.added_params.vector[i]);
for (size_t i = 0; i < p.removed_params.len; i++)
str_append_printf (&message, " %s", p.removed_params.vector[i]);
irc_channel_multicast (p.channel, message.str, NULL);
str_free (&message);
}
mode_processor_free (&p);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
irc_handle_mode (const struct irc_message *msg, struct client *c)
{
if (msg->params.len < 1)
RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
const char *target = msg->params.vector[0];
struct client *client = str_map_find (&c->ctx->users, target);
struct channel *chan = str_map_find (&c->ctx->channels, target);
if (client)
{
if (irc_strcmp (target, c->nickname))
RETURN_WITH_REPLY (c, IRC_ERR_USERSDONTMATCH);
if (msg->params.len < 2)
{
char *mode = client_get_mode (client);
irc_send_reply (c, IRC_RPL_UMODEIS, mode);
free (mode);
}
else
irc_handle_user_mode_change (c, msg->params.vector[1]);
}
else if (chan)
{
if (msg->params.len < 2)
{
char *mode = channel_get_mode (chan, channel_get_user (chan, c));
irc_send_reply (c, IRC_RPL_CHANNELMODEIS, target, mode);
irc_send_reply (c, IRC_RPL_CREATIONTIME,
target, (long long) chan->created);
free (mode);
}
else
irc_handle_chan_mode_change (c, chan, &msg->params.vector[1]);
}
else
irc_send_reply (c, IRC_ERR_NOSUCHNICK, target);
}
static void
irc_handle_user_message (const struct irc_message *msg, struct client *c,
const char *command, bool allow_away_reply)
{
if (msg->params.len < 1)
RETURN_WITH_REPLY (c, IRC_ERR_NORECIPIENT, msg->command);
if (msg->params.len < 2 || !*msg->params.vector[1])
RETURN_WITH_REPLY (c, IRC_ERR_NOTEXTTOSEND);
const char *target = msg->params.vector[0];
const char *text = msg->params.vector[1];
struct client *client = str_map_find (&c->ctx->users, target);
if (client)
{
client_send (client, ":%s!%s@%s %s %s :%s",
c->nickname, c->username, c->hostname, command, target, text);
if (allow_away_reply && client->away_message)
irc_send_reply (c, IRC_RPL_AWAY, target, client->away_message);
// Acknowledging a message from the client to itself would be silly
if (client != c && (c->caps_enabled & IRC_CAP_ECHO_MESSAGE))
client_send (c, ":%s!%s@%s %s %s :%s",
c->nickname, c->username, c->hostname, command, target, text);
return;
}
struct channel *chan = str_map_find (&c->ctx->channels, target);
if (chan)
{
struct channel_user *user = channel_get_user (chan, c);
if ((chan->modes & IRC_CHAN_MODE_NO_OUTSIDE_MSGS) && !user)
RETURN_WITH_REPLY (c, IRC_ERR_CANNOTSENDTOCHAN, target);
if ((chan->modes & IRC_CHAN_MODE_MODERATED) && (!user ||
!(user->modes & (IRC_CHAN_MODE_VOICE | IRC_CHAN_MODE_OPERATOR))))
RETURN_WITH_REPLY (c, IRC_ERR_CANNOTSENDTOCHAN, target);
if (client_in_mask_list (c, &chan->ban_list)
&& !client_in_mask_list (c, &chan->exception_list))
RETURN_WITH_REPLY (c, IRC_ERR_CANNOTSENDTOCHAN, target);
char *message = xstrdup_printf (":%s!%s@%s %s %s :%s",
c->nickname, c->username, c->hostname, command, target, text);
irc_channel_multicast (chan, message,
(c->caps_enabled & IRC_CAP_ECHO_MESSAGE) ? NULL : c);
free (message);
return;
}
irc_send_reply (c, IRC_ERR_NOSUCHNICK, target);
}
static void
irc_handle_privmsg (const struct irc_message *msg, struct client *c)
{
irc_handle_user_message (msg, c, "PRIVMSG", true);
// Let's not care too much about success or failure
c->last_active = time (NULL);
}
static void
irc_handle_notice (const struct irc_message *msg, struct client *c)
{
irc_handle_user_message (msg, c, "NOTICE", false);
}
static void
irc_send_rpl_list (struct client *c, const struct channel *chan)
{
int visible = 0;
for (struct channel_user *user = chan->users;
user; user = user->next)
// XXX: maybe we should skip IRC_USER_MODE_INVISIBLE
visible++;
irc_send_reply (c, IRC_RPL_LIST, chan->name, visible, chan->topic);
}
static void
irc_handle_list (const struct irc_message *msg, struct client *c)
{
if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[1]))
RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[1]);
struct channel *chan;
if (msg->params.len == 0)
{
struct str_map_iter iter = str_map_iter_make (&c->ctx->channels);
while ((chan = str_map_iter_next (&iter)))
if (!(chan->modes & (IRC_CHAN_MODE_PRIVATE | IRC_CHAN_MODE_SECRET))
|| channel_get_user (chan, c))
irc_send_rpl_list (c, chan);
}
else
{
struct strv channels = strv_make ();
cstr_split (msg->params.vector[0], ",", true, &channels);
for (size_t i = 0; i < channels.len; i++)
if ((chan = str_map_find (&c->ctx->channels, channels.vector[i]))
&& (!(chan->modes & IRC_CHAN_MODE_SECRET)
|| channel_get_user (chan, c)))
irc_send_rpl_list (c, chan);
strv_free (&channels);
}
irc_send_reply (c, IRC_RPL_LISTEND);
}
static void
irc_append_prefixes (struct client *c, struct channel_user *user,
struct str *output)
{
struct str prefixes = str_make ();
if (user->modes & IRC_CHAN_MODE_OPERATOR) str_append_c (&prefixes, '@');
if (user->modes & IRC_CHAN_MODE_VOICE) str_append_c (&prefixes, '+');
if (prefixes.len)
{
if (c->caps_enabled & IRC_CAP_MULTI_PREFIX)
str_append (output, prefixes.str);
else
str_append_c (output, prefixes.str[0]);
}
str_free (&prefixes);
}
static char *
irc_make_rpl_namreply_item
(struct client *c, struct client *target, struct channel_user *user)
{
struct str result = str_make ();
if (user)
irc_append_prefixes (c, user, &result);
str_append (&result, target->nickname);
if (c->caps_enabled & IRC_CAP_USERHOST_IN_NAMES)
str_append_printf (&result,
"!%s@%s", target->username, target->hostname);
return str_steal (&result);
}
static void
irc_send_rpl_namreply (struct client *c, const struct channel *chan,
struct str_map *used_nicks)
{
char type = '=';
if (chan->modes & IRC_CHAN_MODE_SECRET)
type = '@';
else if (chan->modes & IRC_CHAN_MODE_PRIVATE)
type = '*';
bool on_channel = channel_get_user (chan, c);
struct strv nicks = strv_make ();
for (struct channel_user *iter = chan->users; iter; iter = iter->next)
{
if (!on_channel && (iter->c->mode & IRC_USER_MODE_INVISIBLE))
continue;
if (used_nicks)
str_map_set (used_nicks, iter->c->nickname, (void *) 1);
strv_append_owned (&nicks,
irc_make_rpl_namreply_item (c, iter->c, iter));
}
irc_send_reply_vector (c, IRC_RPL_NAMREPLY,
nicks.vector, type, chan->name, "");
strv_free (&nicks);
}
static void
irc_send_disassociated_names (struct client *c, struct str_map *used)
{
struct strv nicks = strv_make ();
struct str_map_iter iter = str_map_iter_make (&c->ctx->users);
struct client *target;
while ((target = str_map_iter_next (&iter)))
{
if ((target->mode & IRC_USER_MODE_INVISIBLE)
|| str_map_find (used, target->nickname))
continue;
strv_append_owned (&nicks,
irc_make_rpl_namreply_item (c, target, NULL));
}
if (nicks.len)
irc_send_reply_vector (c, IRC_RPL_NAMREPLY,
nicks.vector, '*', "*", "");
strv_free (&nicks);
}
static void
irc_handle_names (const struct irc_message *msg, struct client *c)
{
if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[1]))
RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[1]);
struct channel *chan;
if (msg->params.len == 0)
{
struct str_map used = str_map_make (NULL);
used.key_xfrm = irc_strxfrm;
struct str_map_iter iter = str_map_iter_make (&c->ctx->channels);
while ((chan = str_map_iter_next (&iter)))
if (!(chan->modes & (IRC_CHAN_MODE_PRIVATE | IRC_CHAN_MODE_SECRET))
|| channel_get_user (chan, c))
irc_send_rpl_namreply (c, chan, &used);
// Also send all visible users we haven't listed yet
irc_send_disassociated_names (c, &used);
str_map_free (&used);
irc_send_reply (c, IRC_RPL_ENDOFNAMES, "*");
}
else
{
struct strv channels = strv_make ();
cstr_split (msg->params.vector[0], ",", true, &channels);
for (size_t i = 0; i < channels.len; i++)
if ((chan = str_map_find (&c->ctx->channels, channels.vector[i]))
&& (!(chan->modes & IRC_CHAN_MODE_SECRET)
|| channel_get_user (chan, c)))
{
irc_send_rpl_namreply (c, chan, NULL);
irc_send_reply (c, IRC_RPL_ENDOFNAMES, channels.vector[i]);
}
strv_free (&channels);
}
}
static void
irc_send_rpl_whoreply (struct client *c, const struct channel *chan,
const struct client *target)
{
struct str chars = str_make ();
str_append_c (&chars, target->away_message ? 'G' : 'H');
if (target->mode & IRC_USER_MODE_OPERATOR)
str_append_c (&chars, '*');
struct channel_user *user;
if (chan && (user = channel_get_user (chan, target)))
irc_append_prefixes (c, user, &chars);
irc_send_reply (c, IRC_RPL_WHOREPLY, chan ? chan->name : "*",
target->username, target->hostname, target->ctx->server_name,
target->nickname, chars.str, 0 /* hop count */, target->realname);
str_free (&chars);
}
static void
irc_match_send_rpl_whoreply (struct client *c, struct client *target,
const char *mask)
{
bool is_roommate = false;
struct str_map_iter iter = str_map_iter_make (&c->ctx->channels);
struct channel *chan;
while ((chan = str_map_iter_next (&iter)))
if (channel_get_user (chan, target) && channel_get_user (chan, c))
{
is_roommate = true;
break;
}
if ((target->mode & IRC_USER_MODE_INVISIBLE) && !is_roommate)
return;
if (irc_fnmatch (mask, target->hostname)
&& irc_fnmatch (mask, target->nickname)
&& irc_fnmatch (mask, target->realname)
&& irc_fnmatch (mask, c->ctx->server_name))
return;
// Try to find a channel they're on that's visible to us
struct channel *user_chan = NULL;
iter = str_map_iter_make (&c->ctx->channels);
while ((chan = str_map_iter_next (&iter)))
if (channel_get_user (chan, target)
&& (!(chan->modes & (IRC_CHAN_MODE_PRIVATE | IRC_CHAN_MODE_SECRET))
|| channel_get_user (chan, c)))
{
user_chan = chan;
break;
}
irc_send_rpl_whoreply (c, user_chan, target);
}
static void
irc_handle_who (const struct irc_message *msg, struct client *c)
{
bool only_ops = msg->params.len > 1 && !strcmp (msg->params.vector[1], "o");
const char *shown_mask = msg->params.vector[0], *used_mask;
if (!shown_mask)
used_mask = shown_mask = "*";
else if (!strcmp (shown_mask, "0"))
used_mask = "*";
else
used_mask = shown_mask;
struct channel *chan;
if ((chan = str_map_find (&c->ctx->channels, used_mask)))
{
bool on_chan = !!channel_get_user (chan, c);
if (on_chan || !(chan->modes & IRC_CHAN_MODE_SECRET))
for (struct channel_user *iter = chan->users;
iter; iter = iter->next)
{
if ((on_chan || !(iter->c->mode & IRC_USER_MODE_INVISIBLE))
&& (!only_ops || (iter->c->mode & IRC_USER_MODE_OPERATOR)))
irc_send_rpl_whoreply (c, chan, iter->c);
}
}
else
{
struct str_map_iter iter = str_map_iter_make (&c->ctx->users);
struct client *target;
while ((target = str_map_iter_next (&iter)))
if (!only_ops || (target->mode & IRC_USER_MODE_OPERATOR))
irc_match_send_rpl_whoreply (c, target, used_mask);
}
irc_send_reply (c, IRC_RPL_ENDOFWHO, shown_mask);
}
static void
irc_send_whois_reply (struct client *c, const struct client *target)
{
const char *nick = target->nickname;
irc_send_reply (c, IRC_RPL_WHOISUSER, nick,
target->username, target->hostname, target->realname);
irc_send_reply (c, IRC_RPL_WHOISSERVER, nick, target->ctx->server_name,
str_map_find (&c->ctx->config, "server_info"));
if (target->mode & IRC_USER_MODE_OPERATOR)
irc_send_reply (c, IRC_RPL_WHOISOPERATOR, nick);
irc_send_reply (c, IRC_RPL_WHOISIDLE, nick,
(int) (time (NULL) - target->last_active));
if (target->away_message)
irc_send_reply (c, IRC_RPL_AWAY, nick, target->away_message);
struct strv channels = strv_make ();
struct str_map_iter iter = str_map_iter_make (&c->ctx->channels);
struct channel *chan;
struct channel_user *channel_user;
while ((chan = str_map_iter_next (&iter)))
if ((channel_user = channel_get_user (chan, target))
&& (!(chan->modes & (IRC_CHAN_MODE_PRIVATE | IRC_CHAN_MODE_SECRET))
|| channel_get_user (chan, c)))
{
struct str item = str_make ();
if (channel_user->modes & IRC_CHAN_MODE_OPERATOR)
str_append_c (&item, '@');
else if (channel_user->modes & IRC_CHAN_MODE_VOICE)
str_append_c (&item, '+');
str_append (&item, chan->name);
strv_append_owned (&channels, str_steal (&item));
}
irc_send_reply_vector (c, IRC_RPL_WHOISCHANNELS, channels.vector, nick, "");
strv_free (&channels);
irc_send_reply (c, IRC_RPL_ENDOFWHOIS, nick);
}
static void
irc_handle_whois (const struct irc_message *msg, struct client *c)
{
if (msg->params.len < 1)
RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[0]))
RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[0]);
struct strv masks = strv_make ();
const char *masks_str = msg->params.vector[msg->params.len > 1];
cstr_split (masks_str, ",", true, &masks);
for (size_t i = 0; i < masks.len; i++)
{
const char *mask = masks.vector[i];
struct client *target;
if (!strpbrk (mask, "*?"))
{
if (!(target = str_map_find (&c->ctx->users, mask)))
irc_send_reply (c, IRC_ERR_NOSUCHNICK, mask);
else
irc_send_whois_reply (c, target);
}
else
{
struct str_map_iter iter = str_map_iter_make (&c->ctx->users);
bool found = false;
while ((target = str_map_iter_next (&iter)))
if (!irc_fnmatch (mask, target->nickname))
{
irc_send_whois_reply (c, target);
found = true;
}
if (!found)
irc_send_reply (c, IRC_ERR_NOSUCHNICK, mask);
}
}
strv_free (&masks);
}
static void
irc_handle_whowas (const struct irc_message *msg, struct client *c)
{
if (msg->params.len < 1)
RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
if (msg->params.len > 2 && !irc_is_this_me (c->ctx, msg->params.vector[2]))
RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[2]);
// The "count" parameter is ignored, we only store one entry for a nick
struct strv nicks = strv_make ();
cstr_split (msg->params.vector[0], ",", true, &nicks);
for (size_t i = 0; i < nicks.len; i++)
{
const char *nick = nicks.vector[i];
struct whowas_info *info = str_map_find (&c->ctx->whowas, nick);
if (!info)
irc_send_reply (c, IRC_ERR_WASNOSUCHNICK, nick);
else
{
irc_send_reply (c, IRC_RPL_WHOWASUSER, nick,
info->username, info->hostname, info->realname);
irc_send_reply (c, IRC_RPL_WHOISSERVER, nick, c->ctx->server_name,
str_map_find (&c->ctx->config, "server_info"));
}
irc_send_reply (c, IRC_RPL_ENDOFWHOWAS, nick);
}
strv_free (&nicks);
}
static void
irc_send_rpl_topic (struct client *c, struct channel *chan)
{
if (!*chan->topic)
irc_send_reply (c, IRC_RPL_NOTOPIC, chan->name);
else
{
irc_send_reply (c, IRC_RPL_TOPIC, chan->name, chan->topic);
irc_send_reply (c, IRC_RPL_TOPICWHOTIME,
chan->name, chan->topic_who, (long long) chan->topic_time);
}
}
static void
irc_handle_topic (const struct irc_message *msg, struct client *c)
{
if (msg->params.len < 1)
RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
const char *target = msg->params.vector[0];
struct channel *chan = str_map_find (&c->ctx->channels, target);
if (!chan)
RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHCHANNEL, target);
if (msg->params.len < 2)
{
irc_send_rpl_topic (c, chan);
return;
}
struct channel_user *user = channel_get_user (chan, c);
if (!user)
RETURN_WITH_REPLY (c, IRC_ERR_NOTONCHANNEL, target);
if ((chan->modes & IRC_CHAN_MODE_PROTECTED_TOPIC)
&& !(user->modes & IRC_CHAN_MODE_OPERATOR))
RETURN_WITH_REPLY (c, IRC_ERR_CHANOPRIVSNEEDED, target);
cstr_set (&chan->topic, xstrdup (msg->params.vector[1]));
cstr_set (&chan->topic_who, xstrdup_printf
("%s!%s@%s", c->nickname, c->username, c->hostname));
chan->topic_time = time (NULL);
char *message = xstrdup_printf (":%s!%s@%s TOPIC %s :%s",
c->nickname, c->username, c->hostname, target, chan->topic);
irc_channel_multicast (chan, message, NULL);
free (message);
}
static void
irc_try_part (struct client *c, const char *channel_name, const char *reason)
{
if (!reason)
reason = c->nickname;
struct channel *chan;
if (!(chan = str_map_find (&c->ctx->channels, channel_name)))
RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHCHANNEL, channel_name);
struct channel_user *user;
if (!(user = channel_get_user (chan, c)))
RETURN_WITH_REPLY (c, IRC_ERR_NOTONCHANNEL, channel_name);
char *message = xstrdup_printf (":%s!%s@%s PART %s :%s",
c->nickname, c->username, c->hostname, channel_name, reason);
if (!(chan->modes & IRC_CHAN_MODE_QUIET))
irc_channel_multicast (chan, message, NULL);
else
client_send (c, "%s", message);
free (message);
channel_remove_user (chan, user);
irc_channel_destroy_if_empty (c->ctx, chan);
}
static void
irc_part_all_channels (struct client *c)
{
// We have to be careful here, the channel might get destroyed
struct str_map_unset_iter iter =
str_map_unset_iter_make (&c->ctx->channels);
struct channel *chan;
while ((chan = str_map_unset_iter_next (&iter)))
if (channel_get_user (chan, c))
irc_try_part (c, chan->name, NULL);
str_map_unset_iter_free (&iter);
}
static void
irc_handle_part (const struct irc_message *msg, struct client *c)
{
if (msg->params.len < 1)
RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
const char *reason = msg->params.len > 1 ? msg->params.vector[1] : NULL;
struct strv channels = strv_make ();
cstr_split (msg->params.vector[0], ",", true, &channels);
for (size_t i = 0; i < channels.len; i++)
irc_try_part (c, channels.vector[i], reason);
strv_free (&channels);
}
static void
irc_try_kick (struct client *c, const char *channel_name, const char *nick,
const char *reason)
{
struct channel *chan;
if (!(chan = str_map_find (&c->ctx->channels, channel_name)))
RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHCHANNEL, channel_name);
struct channel_user *user;
if (!(user = channel_get_user (chan, c)))
RETURN_WITH_REPLY (c, IRC_ERR_NOTONCHANNEL, channel_name);
if (!(user->modes & IRC_CHAN_MODE_OPERATOR))
RETURN_WITH_REPLY (c, IRC_ERR_CHANOPRIVSNEEDED, channel_name);
struct client *client;
if (!(client = str_map_find (&c->ctx->users, nick))
|| !(user = channel_get_user (chan, client)))
RETURN_WITH_REPLY (c, IRC_ERR_USERNOTINCHANNEL, nick, channel_name);
char *message = xstrdup_printf (":%s!%s@%s KICK %s %s :%s",
c->nickname, c->username, c->hostname, channel_name, nick, reason);
if (!(chan->modes & IRC_CHAN_MODE_QUIET))
irc_channel_multicast (chan, message, NULL);
else
client_send (c, "%s", message);
free (message);
channel_remove_user (chan, user);
irc_channel_destroy_if_empty (c->ctx, chan);
}
static void
irc_handle_kick (const struct irc_message *msg, struct client *c)
{
if (msg->params.len < 2)
RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
const char *reason = c->nickname;
if (msg->params.len > 2)
reason = msg->params.vector[2];
struct strv channels = strv_make ();
cstr_split (msg->params.vector[0], ",", true, &channels);
struct strv users = strv_make ();
cstr_split (msg->params.vector[1], ",", true, &users);
if (channels.len == 1)
for (size_t i = 0; i < users.len; i++)
irc_try_kick (c, channels.vector[0], users.vector[i], reason);
else
for (size_t i = 0; i < channels.len && i < users.len; i++)
irc_try_kick (c, channels.vector[i], users.vector[i], reason);
strv_free (&channels);
strv_free (&users);
}
static void
irc_send_invite_notifications
(struct channel *chan, struct client *c, struct client *target)
{
for (struct channel_user *iter = chan->users; iter; iter = iter->next)
if (iter->c != target && iter->c->caps_enabled & IRC_CAP_INVITE_NOTIFY)
client_send (iter->c, ":%s!%s@%s INVITE %s %s",
c->nickname, c->username, c->hostname,
target->nickname, chan->name);
}
static void
irc_handle_invite (const struct irc_message *msg, struct client *c)
{
if (msg->params.len < 2)
RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
const char *target = msg->params.vector[0];
const char *channel_name = msg->params.vector[1];
struct client *client = str_map_find (&c->ctx->users, target);
if (!client)
RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHNICK, target);
struct channel *chan = str_map_find (&c->ctx->channels, channel_name);
if (chan)
{
struct channel_user *inviting_user;
if (!(inviting_user = channel_get_user (chan, c)))
RETURN_WITH_REPLY (c, IRC_ERR_NOTONCHANNEL, channel_name);
if (channel_get_user (chan, client))
RETURN_WITH_REPLY (c, IRC_ERR_USERONCHANNEL, target, channel_name);
if ((inviting_user->modes & IRC_CHAN_MODE_OPERATOR))
str_map_set (&client->invites, channel_name, (void *) 1);
else if ((chan->modes & IRC_CHAN_MODE_INVITE_ONLY))
RETURN_WITH_REPLY (c, IRC_ERR_CHANOPRIVSNEEDED, channel_name);
// It's not specified when and how we should send out invite-notify
if (chan->modes & IRC_CHAN_MODE_INVITE_ONLY)
irc_send_invite_notifications (chan, c, client);
}
client_send (client, ":%s!%s@%s INVITE %s %s",
c->nickname, c->username, c->hostname, client->nickname, channel_name);
if (client->away_message)
irc_send_reply (c, IRC_RPL_AWAY,
client->nickname, client->away_message);
irc_send_reply (c, IRC_RPL_INVITING, client->nickname, channel_name);
}
static void
irc_try_join (struct client *c, const char *channel_name, const char *key)
{
struct channel *chan = str_map_find (&c->ctx->channels, channel_name);
unsigned user_mode = 0;
if (!chan)
{
if (irc_validate_channel_name (channel_name) != VALIDATION_OK)
RETURN_WITH_REPLY (c, IRC_ERR_BADCHANMASK, channel_name);
chan = irc_channel_create (c->ctx, channel_name);
user_mode = IRC_CHAN_MODE_OPERATOR;
}
else if (channel_get_user (chan, c))
return;
bool invited_by_chanop = !!str_map_find (&c->invites, channel_name);
if ((chan->modes & IRC_CHAN_MODE_INVITE_ONLY)
&& !client_in_mask_list (c, &chan->invite_list)
&& !invited_by_chanop)
RETURN_WITH_REPLY (c, IRC_ERR_INVITEONLYCHAN, channel_name);
if (chan->key && (!key || strcmp (key, chan->key)))
RETURN_WITH_REPLY (c, IRC_ERR_BADCHANNELKEY, channel_name);
if (chan->user_limit != -1
&& channel_user_count (chan) >= (size_t) chan->user_limit)
RETURN_WITH_REPLY (c, IRC_ERR_CHANNELISFULL, channel_name);
if (client_in_mask_list (c, &chan->ban_list)
&& !client_in_mask_list (c, &chan->exception_list)
&& !invited_by_chanop)
RETURN_WITH_REPLY (c, IRC_ERR_BANNEDFROMCHAN, channel_name);
// Destroy any invitation as there's no other way to get rid of it
str_map_set (&c->invites, channel_name, NULL);
channel_add_user (chan, c)->modes = user_mode;
char *message = xstrdup_printf (":%s!%s@%s JOIN %s",
c->nickname, c->username, c->hostname, channel_name);
if (!(chan->modes & IRC_CHAN_MODE_QUIET))
irc_channel_multicast (chan, message, NULL);
else
client_send (c, "%s", message);
free (message);
irc_send_rpl_topic (c, chan);
irc_send_rpl_namreply (c, chan, NULL);
irc_send_reply (c, IRC_RPL_ENDOFNAMES, chan->name);
}
static void
irc_handle_join (const struct irc_message *msg, struct client *c)
{
if (msg->params.len < 1)
RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
if (!strcmp (msg->params.vector[0], "0"))
{
irc_part_all_channels (c);
return;
}
struct strv channels = strv_make ();
cstr_split (msg->params.vector[0], ",", true, &channels);
struct strv keys = strv_make ();
if (msg->params.len > 1)
cstr_split (msg->params.vector[1], ",", true, &keys);
for (size_t i = 0; i < channels.len; i++)
irc_try_join (c, channels.vector[i],
i < keys.len ? keys.vector[i] : NULL);
strv_free (&channels);
strv_free (&keys);
}
static void
irc_handle_summon (const struct irc_message *msg, struct client *c)
{
(void) msg;
irc_send_reply (c, IRC_ERR_SUMMONDISABLED);
}
static void
irc_handle_users (const struct irc_message *msg, struct client *c)
{
(void) msg;
irc_send_reply (c, IRC_ERR_USERSDISABLED);
}
static void
irc_handle_away (const struct irc_message *msg, struct client *c)
{
if (msg->params.len < 1)
{
cstr_set (&c->away_message, NULL);
irc_send_reply (c, IRC_RPL_UNAWAY);
}
else
{
cstr_set (&c->away_message, xstrdup (msg->params.vector[0]));
irc_send_reply (c, IRC_RPL_NOWAWAY);
}
}
static void
irc_handle_ison (const struct irc_message *msg, struct client *c)
{
if (msg->params.len < 1)
RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
struct str result = str_make ();
const char *nick;
if (str_map_find (&c->ctx->users, (nick = msg->params.vector[0])))
str_append (&result, nick);
for (size_t i = 1; i < msg->params.len; i++)
if (str_map_find (&c->ctx->users, (nick = msg->params.vector[i])))
str_append_printf (&result, " %s", nick);
irc_send_reply (c, IRC_RPL_ISON, result.str);
str_free (&result);
}
static void
irc_handle_admin (const struct irc_message *msg, struct client *c)
{
if (msg->params.len > 0 && !irc_is_this_me (c->ctx, msg->params.vector[0]))
RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[0]);
irc_send_reply (c, IRC_ERR_NOADMININFO, c->ctx->server_name);
}
static void
irc_handle_stats_links (struct client *c, const struct irc_message *msg)
{
// There is only an "l" query in RFC 2812 but we cannot link,
// so instead we provide the "L" query giving information for all users
const char *filter = NULL;
if (msg->params.len > 1)
filter = msg->params.vector[1];
for (struct client *iter = c->ctx->clients; iter; iter = iter->next)
{
if (filter && irc_strcmp (iter->nickname, filter))
continue;
irc_send_reply (c, IRC_RPL_STATSLINKINFO,
iter->address, // linkname
iter->write_buffer.len, // sendq
iter->n_sent_messages, iter->sent_bytes / 1024,
iter->n_received_messages, iter->received_bytes / 1024,
(long long) (time (NULL) - iter->opened));
}
}
static void
irc_handle_stats_commands (struct client *c)
{
struct str_map_iter iter = str_map_iter_make (&c->ctx->handlers);
struct irc_command *handler;
while ((handler = str_map_iter_next (&iter)))
{
if (!handler->n_received)
continue;
irc_send_reply (c, IRC_RPL_STATSCOMMANDS, handler->name,
handler->n_received, handler->bytes_received, (size_t) 0);
}
}
static void
irc_handle_stats_uptime (struct client *c)
{
time_t uptime = time (NULL) - c->ctx->started;
int days = uptime / 60 / 60 / 24;
int hours = (uptime % (60 * 60 * 24)) / 60 / 60;
int mins = (uptime % (60 * 60)) / 60;
int secs = uptime % 60;
irc_send_reply (c, IRC_RPL_STATSUPTIME, days, hours, mins, secs);
}
static void
irc_handle_stats (const struct irc_message *msg, struct client *c)
{
char query = 0;
if (msg->params.len > 0)
query = *msg->params.vector[0];
if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[1]))
RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[1]);
if (!(c->mode & IRC_USER_MODE_OPERATOR))
RETURN_WITH_REPLY (c, IRC_ERR_NOPRIVILEGES);
switch (query)
{
case 'L': irc_handle_stats_links (c, msg); break;
case 'm': irc_handle_stats_commands (c); break;
case 'u': irc_handle_stats_uptime (c); break;
}
irc_send_reply (c, IRC_RPL_ENDOFSTATS, query ? query : '*');
}
static void
irc_handle_links (const struct irc_message *msg, struct client *c)
{
if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[0]))
RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[0]);
const char *mask = "*";
if (msg->params.len > 0)
mask = msg->params.vector[msg->params.len > 1];
if (!irc_fnmatch (mask, c->ctx->server_name))
irc_send_reply (c, IRC_RPL_LINKS, mask,
c->ctx->server_name, 0 /* hop count */,
str_map_find (&c->ctx->config, "server_info"));
irc_send_reply (c, IRC_RPL_ENDOFLINKS, mask);
}
static void
irc_handle_kill (const struct irc_message *msg, struct client *c)
{
if (msg->params.len < 2)
RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
if (!(c->mode & IRC_USER_MODE_OPERATOR))
RETURN_WITH_REPLY (c, IRC_ERR_NOPRIVILEGES);
struct client *target;
if (!(target = str_map_find (&c->ctx->users, msg->params.vector[0])))
RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHNICK, msg->params.vector[0]);
client_send (target, ":%s!%s@%s KILL %s :%s",
c->nickname, c->username, c->hostname,
target->nickname, msg->params.vector[1]);
char *reason = xstrdup_printf ("Killed by %s: %s",
c->nickname, msg->params.vector[1]);
client_close_link (target, reason);
free (reason);
}
static void
irc_handle_die (const struct irc_message *msg, struct client *c)
{
(void) msg;
if (!(c->mode & IRC_USER_MODE_OPERATOR))
RETURN_WITH_REPLY (c, IRC_ERR_NOPRIVILEGES);
if (!c->ctx->quitting)
irc_initiate_quit (c->ctx);
}
// -----------------------------------------------------------------------------
static void
irc_register_handlers (struct server_context *ctx)
{
// TODO: add an index for IRC_ERR_NOSUCHSERVER validation?
// TODO: add a minimal parameter count?
// TODO: add a field for oper-only commands?
static struct irc_command message_handlers[] =
{
{ "CAP", false, irc_handle_cap, 0, 0 },
{ "PASS", false, irc_handle_pass, 0, 0 },
{ "NICK", false, irc_handle_nick, 0, 0 },
{ "USER", false, irc_handle_user, 0, 0 },
{ "USERHOST", true, irc_handle_userhost, 0, 0 },
{ "LUSERS", true, irc_handle_lusers, 0, 0 },
{ "MOTD", true, irc_handle_motd, 0, 0 },
{ "PING", true, irc_handle_ping, 0, 0 },
{ "PONG", false, irc_handle_pong, 0, 0 },
{ "QUIT", false, irc_handle_quit, 0, 0 },
{ "TIME", true, irc_handle_time, 0, 0 },
{ "VERSION", true, irc_handle_version, 0, 0 },
{ "USERS", true, irc_handle_users, 0, 0 },
{ "SUMMON", true, irc_handle_summon, 0, 0 },
{ "AWAY", true, irc_handle_away, 0, 0 },
{ "ADMIN", true, irc_handle_admin, 0, 0 },
{ "STATS", true, irc_handle_stats, 0, 0 },
{ "LINKS", true, irc_handle_links, 0, 0 },
{ "MODE", true, irc_handle_mode, 0, 0 },
{ "PRIVMSG", true, irc_handle_privmsg, 0, 0 },
{ "NOTICE", true, irc_handle_notice, 0, 0 },
{ "JOIN", true, irc_handle_join, 0, 0 },
{ "PART", true, irc_handle_part, 0, 0 },
{ "KICK", true, irc_handle_kick, 0, 0 },
{ "INVITE", true, irc_handle_invite, 0, 0 },
{ "TOPIC", true, irc_handle_topic, 0, 0 },
{ "LIST", true, irc_handle_list, 0, 0 },
{ "NAMES", true, irc_handle_names, 0, 0 },
{ "WHO", true, irc_handle_who, 0, 0 },
{ "WHOIS", true, irc_handle_whois, 0, 0 },
{ "WHOWAS", true, irc_handle_whowas, 0, 0 },
{ "ISON", true, irc_handle_ison, 0, 0 },
{ "KILL", true, irc_handle_kill, 0, 0 },
{ "DIE", true, irc_handle_die, 0, 0 },
};
for (size_t i = 0; i < N_ELEMENTS (message_handlers); i++)
{
const struct irc_command *cmd = &message_handlers[i];
str_map_set (&ctx->handlers, cmd->name, (void *) cmd);
}
}
static void
irc_process_message (const struct irc_message *msg,
const char *raw, void *user_data)
{
struct client *c = user_data;
if (c->closing_link)
return;
c->n_received_messages++;
c->received_bytes += strlen (raw) + 2;
if (!flood_detector_check (&c->antiflood))
{
client_close_link (c, "Excess flood");
return;
}
struct irc_command *cmd = str_map_find (&c->ctx->handlers, msg->command);
if (!cmd)
irc_send_reply (c, IRC_ERR_UNKNOWNCOMMAND, msg->command);
else
{
cmd->n_received++;
cmd->bytes_received += strlen (raw) + 2;
if (cmd->requires_registration && !c->registered)
irc_send_reply (c, IRC_ERR_NOTREGISTERED);
else
cmd->handler (msg, c);
}
}
// --- Network I/O -------------------------------------------------------------
static bool
irc_try_read (struct client *c)
{
struct str *buf = &c->read_buffer;
ssize_t n_read;
while (true)
{
str_reserve (buf, 512);
n_read = read (c->socket_fd, buf->str + buf->len,
buf->alloc - buf->len - 1 /* null byte */);
if (n_read > 0)
{
buf->str[buf->len += n_read] = '\0';
// TODO: discard characters above the 512 character limit
irc_process_buffer (buf, irc_process_message, c);
continue;
}
if (n_read == 0)
{
client_kill (c, NULL);
return false;
}
if (errno == EAGAIN)
return true;
if (errno == EINTR)
continue;
print_debug ("%s: %s: %s", __func__, "read", strerror (errno));
client_kill (c, strerror (errno));
return false;
}
}
static bool
irc_try_read_tls (struct client *c)
{
if (c->ssl_tx_want_rx)
return true;
struct str *buf = &c->read_buffer;
c->ssl_rx_want_tx = false;
while (true)
{
str_reserve (buf, 512);
ERR_clear_error ();
int n_read = SSL_read (c->ssl, buf->str + buf->len,
buf->alloc - buf->len - 1 /* null byte */);
const char *error_info = NULL;
switch (xssl_get_error (c->ssl, n_read, &error_info))
{
case SSL_ERROR_NONE:
buf->str[buf->len += n_read] = '\0';
// TODO: discard characters above the 512 character limit
irc_process_buffer (buf, irc_process_message, c);
continue;
case SSL_ERROR_ZERO_RETURN:
client_kill (c, NULL);
return false;
case SSL_ERROR_WANT_READ:
return true;
case SSL_ERROR_WANT_WRITE:
c->ssl_rx_want_tx = true;
return true;
case XSSL_ERROR_TRY_AGAIN:
continue;
default:
print_debug ("%s: %s: %s", __func__, "SSL_read", error_info);
client_kill (c, error_info);
return false;
}
}
}
static bool
irc_try_write (struct client *c)
{
struct str *buf = &c->write_buffer;
ssize_t n_written;
while (buf->len)
{
n_written = write (c->socket_fd, buf->str, buf->len);
if (n_written >= 0)
{
str_remove_slice (buf, 0, n_written);
continue;
}
if (errno == EAGAIN)
return true;
if (errno == EINTR)
continue;
print_debug ("%s: %s: %s", __func__, "write", strerror (errno));
client_kill (c, strerror (errno));
return false;
}
return true;
}
static bool
irc_try_write_tls (struct client *c)
{
if (c->ssl_rx_want_tx)
return true;
struct str *buf = &c->write_buffer;
c->ssl_tx_want_rx = false;
while (buf->len)
{
ERR_clear_error ();
int n_written = SSL_write (c->ssl, buf->str, buf->len);
const char *error_info = NULL;
switch (xssl_get_error (c->ssl, n_written, &error_info))
{
case SSL_ERROR_NONE:
str_remove_slice (buf, 0, n_written);
continue;
case SSL_ERROR_ZERO_RETURN:
client_kill (c, NULL);
return false;
case SSL_ERROR_WANT_WRITE:
return true;
case SSL_ERROR_WANT_READ:
c->ssl_tx_want_rx = true;
return true;
case XSSL_ERROR_TRY_AGAIN:
continue;
default:
print_debug ("%s: %s: %s", __func__, "SSL_write", error_info);
client_kill (c, error_info);
return false;
}
}
return true;
}
// -----------------------------------------------------------------------------
static bool
irc_autodetect_tls (struct client *c)
{
// Trivial SSL/TLS autodetection. The first block of data returned by
// recv() must be at least three bytes long for this to work reliably,
// but that should not pose a problem in practice.
//
// SSL2: 1xxx xxxx | xxxx xxxx | <1>
// (message length) (client hello)
// SSL3/TLS: <22> | <3> | xxxx xxxx
// (handshake)| (protocol version)
//
// Such byte sequences should never occur at the beginning of regular IRC
// communication, which usually begins with USER/NICK/PASS/SERVICE.
char buf[3];
start:
switch (recv (c->socket_fd, buf, sizeof buf, MSG_PEEK))
{
case 3:
if ((buf[0] & 0x80) && buf[2] == 1)
return true;
// Fall-through
case 2:
if (buf[0] == 22 && buf[1] == 3)
return true;
break;
case 1:
if (buf[0] == 22)
return true;
break;
case 0:
break;
default:
if (errno == EINTR)
goto start;
}
return false;
}
static bool
client_initialize_tls (struct client *c)
{
const char *error_info = NULL;
if (!c->ctx->ssl_ctx)
{
error_info = "TLS support disabled";
goto error_ssl_1;
}
ERR_clear_error ();
c->ssl = SSL_new (c->ctx->ssl_ctx);
if (!c->ssl)
goto error_ssl_2;
if (!SSL_set_fd (c->ssl, c->socket_fd))
goto error_ssl_3;
SSL_set_accept_state (c->ssl);
return true;
error_ssl_3:
SSL_free (c->ssl);
c->ssl = NULL;
error_ssl_2:
error_info = xerr_describe_error ();
error_ssl_1:
print_debug ("could not initialize TLS for %s: %s", c->address, error_info);
return false;
}
// -----------------------------------------------------------------------------
static void
on_client_ready (const struct pollfd *pfd, void *user_data)
{
struct client *c = user_data;
if (!c->initialized)
{
hard_assert (pfd->events == POLLIN);
if (irc_autodetect_tls (c) && !client_initialize_tls (c))
{
client_kill (c, NULL);
return;
}
c->initialized = true;
client_set_ping_timer (c);
}
if (c->ssl)
{
// Reads may want to write, writes may want to read, poll() may
// return unexpected things in `revents'... let's try both
if (!irc_try_read_tls (c) || !irc_try_write_tls (c))
return;
}
else if (!irc_try_read (c) || !irc_try_write (c))
return;
client_update_poller (c, pfd);
// The purpose of the `closing_link' state is to transfer the `ERROR'
if (c->closing_link && !c->half_closed && !c->write_buffer.len)
{
// To make sure the client has received our ERROR message, we must
// first half-close the connection, otherwise it could happen that they
// receive a RST from our TCP stack first when we receive further data
// We only send the "close notify" alert if libssl can write to the
// socket at this moment. All the other data has been already written,
// though, and the client will receive a TCP half-close as usual, so
// it's not that important if the alert actually gets through.
if (c->ssl)
(void) SSL_shutdown (c->ssl);
// Either the shutdown succeeds, in which case we set a flag so that
// we don't retry this action and wait until we get an EOF, or it fails
// and we just kill the client straight away
if (!shutdown (c->socket_fd, SHUT_WR))
c->half_closed = true;
else
client_kill (c, NULL);
}
}
static void
client_update_poller (struct client *c, const struct pollfd *pfd)
{
// We must not poll for writing when the connection hasn't been initialized
int new_events = POLLIN;
if (c->ssl)
{
if (c->write_buffer.len || c->ssl_rx_want_tx)
new_events |= POLLOUT;
// While we're waiting for an opposite event, we ignore the original
if (c->ssl_rx_want_tx) new_events &= ~POLLIN;
if (c->ssl_tx_want_rx) new_events &= ~POLLOUT;
}
else if (c->initialized && c->write_buffer.len)
new_events |= POLLOUT;
hard_assert (new_events != 0);
if (!pfd || pfd->events != new_events)
poller_fd_set (&c->socket_event, new_events);
}
static void
client_finish_connection (struct client *c)
{
c->gni = NULL;
c->address = format_host_port_pair (c->hostname, c->port);
print_debug ("accepted connection from %s", c->address);
client_update_poller (c, NULL);
client_set_kill_timer (c);
}
static void
on_client_gni_resolved (int result, char *host, char *port, void *user_data)
{
struct client *c = user_data;
if (result)
print_debug ("%s: %s", "getnameinfo", gai_strerror (result));
else
{
cstr_set (&c->hostname, xstrdup (host));
(void) port;
}
poller_timer_reset (&c->gni_timer);
client_finish_connection (c);
}
static void
on_client_gni_timer (struct client *c)
{
async_cancel (&c->gni->async);
client_finish_connection (c);
}
static bool
irc_try_fetch_client (struct server_context *ctx, int listen_fd)
{
// XXX: `struct sockaddr_storage' is not the most portable thing
struct sockaddr_storage peer;
socklen_t peer_len = sizeof peer;
int fd = accept (listen_fd, (struct sockaddr *) &peer, &peer_len);
if (fd == -1)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
return false;
if (errno == EINTR)
return true;
if (errno == EBADF
|| errno == EINVAL
|| errno == ENOTSOCK
|| errno == EOPNOTSUPP)
print_fatal ("%s: %s", "accept", strerror (errno));
// OS kernels may return a wide range of unforeseeable errors.
// Assuming that they're either transient or caused by
// a connection that we've just extracted from the queue.
print_warning ("%s: %s", "accept", strerror (errno));
return true;
}
hard_assert (peer_len <= sizeof peer);
set_blocking (fd, false);
// A little bit questionable once the traffic gets high enough (IMO),
// but it reduces silly latencies that we don't need because we already
// do buffer our output
int yes = 1;
soft_assert (setsockopt (fd, IPPROTO_TCP, TCP_NODELAY,
&yes, sizeof yes) != -1);
if (ctx->max_connections != 0 && ctx->n_clients >= ctx->max_connections)
{
print_debug ("connection limit reached, refusing connection");
close (fd);
return true;
}
char host[NI_MAXHOST] = "unknown", port[NI_MAXSERV] = "unknown";
int err = getnameinfo ((struct sockaddr *) &peer, peer_len,
host, sizeof host, port, sizeof port, NI_NUMERICHOST | NI_NUMERICSERV);
if (err)
print_debug ("%s: %s", "getnameinfo", gai_strerror (err));
struct client *c = client_new ();
c->ctx = ctx;
c->opened = time (NULL);
c->socket_fd = fd;
c->hostname = xstrdup (host);
c->port = xstrdup (port);
c->last_active = time (NULL);
LIST_PREPEND (ctx->clients, c);
ctx->n_clients++;
c->socket_event = poller_fd_make (&c->ctx->poller, c->socket_fd);
c->socket_event.dispatcher = (poller_fd_fn) on_client_ready;
c->socket_event.user_data = c;
c->kill_timer = poller_timer_make (&c->ctx->poller);
c->kill_timer.dispatcher = (poller_timer_fn) on_client_kill_timer;
c->kill_timer.user_data = c;
c->timeout_timer = poller_timer_make (&c->ctx->poller);
c->timeout_timer.dispatcher = (poller_timer_fn) on_client_timeout_timer;
c->timeout_timer.user_data = c;
c->ping_timer = poller_timer_make (&c->ctx->poller);
c->ping_timer.dispatcher = (poller_timer_fn) on_client_ping_timer;
c->ping_timer.user_data = c;
// Resolve the client's hostname first; this is a blocking operation that
// depends on the network, so run it asynchronously with some timeout
c->gni = async_getnameinfo (&ctx->poller.common.async,
(const struct sockaddr *) &peer, peer_len, NI_NUMERICSERV);
c->gni->dispatcher = on_client_gni_resolved;
c->gni->user_data = c;
c->gni_timer = poller_timer_make (&c->ctx->poller);
c->gni_timer.dispatcher = (poller_timer_fn) on_client_gni_timer;
c->gni_timer.user_data = c;
poller_timer_set (&c->gni_timer, 5000);
return true;
}
static void
on_irc_client_available (const struct pollfd *pfd, void *user_data)
{
struct server_context *ctx = user_data;
while (irc_try_fetch_client (ctx, pfd->fd))
;
}
// --- Application setup -------------------------------------------------------
static int
irc_ssl_verify_callback (int verify_ok, X509_STORE_CTX *ctx)
{
(void) verify_ok;
(void) ctx;
// RFC 5246: "If the client has sent a certificate with signing ability,
// a digitally-signed CertificateVerify message is sent to explicitly
// verify possession of the private key in the certificate."
//
// The handshake will fail if the client doesn't have a matching private
// key, see OpenSSL's tls_process_cert_verify(), and the CertificateVerify
// message cannot be skipped (except for a case where it doesn't matter).
// Thus we're fine checking just the cryptographic hash of the certificate.
// We only want to provide additional privileges based on the client's
// certificate, so let's not terminate the connection because of a failure
// (especially since self-signed certificates are likely to be used).
return 1;
}
static void
irc_ssl_info_callback (const SSL *ssl, int where, int ret)
{
// For debugging only; provides us with the most important information
struct str s = str_make ();
if (where & SSL_CB_LOOP)
str_append_printf (&s, "loop (%s) ",
SSL_state_string_long (ssl));
if (where & SSL_CB_EXIT)
str_append_printf (&s, "exit (%d in %s) ", ret,
SSL_state_string_long (ssl));
if (where & SSL_CB_READ) str_append (&s, "read ");
if (where & SSL_CB_WRITE) str_append (&s, "write ");
if (where & SSL_CB_ALERT)
str_append_printf (&s, "alert (%s: %s) ",
SSL_alert_type_string_long (ret),
SSL_alert_desc_string_long (ret));
if (where & SSL_CB_HANDSHAKE_START) str_append (&s, "handshake start ");
if (where & SSL_CB_HANDSHAKE_DONE) str_append (&s, "handshake done ");
print_debug ("ssl <%p> %s", ssl, s.str);
str_free (&s);
}
static bool
irc_initialize_ssl_ctx (struct server_context *ctx,
const char *cert_path, const char *key_path, struct error **e)
{
ERR_clear_error ();
ctx->ssl_ctx = SSL_CTX_new (SSLv23_server_method ());
if (!ctx->ssl_ctx)
{
error_set (e, "%s: %s", "could not initialize TLS",
xerr_describe_error ());
return false;
}
SSL_CTX_set_verify (ctx->ssl_ctx,
SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, irc_ssl_verify_callback);
if (g_debug_mode)
SSL_CTX_set_info_callback (ctx->ssl_ctx, irc_ssl_info_callback);
const unsigned char session_id_context[SSL_MAX_SSL_SESSION_ID_LENGTH]
= PROGRAM_NAME;
(void) SSL_CTX_set_session_id_context (ctx->ssl_ctx,
session_id_context, sizeof session_id_context);
// IRC is not particularly reconnect-heavy, prefer forward secrecy
SSL_CTX_set_session_cache_mode (ctx->ssl_ctx, SSL_SESS_CACHE_OFF);
// Gah, spare me your awkward semantics, I just want to push data!
SSL_CTX_set_mode (ctx->ssl_ctx,
SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | SSL_MODE_ENABLE_PARTIAL_WRITE);
// Disable deprecated protocols (see RFC 7568)
SSL_CTX_set_options (ctx->ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
// XXX: perhaps we should read the files ourselves for better messages
const char *ciphers = str_map_find (&ctx->config, "tls_ciphers");
if (!SSL_CTX_set_cipher_list (ctx->ssl_ctx, ciphers))
error_set (e, "failed to select any cipher from the cipher list");
else if (!SSL_CTX_use_certificate_chain_file (ctx->ssl_ctx, cert_path))
error_set (e, "%s: %s", "setting the TLS certificate failed",
xerr_describe_error ());
else if (!SSL_CTX_use_PrivateKey_file
(ctx->ssl_ctx, key_path, SSL_FILETYPE_PEM))
error_set (e, "%s: %s", "setting the TLS private key failed",
xerr_describe_error ());
else
// TODO: SSL_CTX_check_private_key()? It has probably already been
// checked by SSL_CTX_use_PrivateKey_file() above.
return true;
SSL_CTX_free (ctx->ssl_ctx);
ctx->ssl_ctx = NULL;
return false;
}
static bool
irc_initialize_tls (struct server_context *ctx, struct error **e)
{
const char *tls_cert = str_map_find (&ctx->config, "tls_cert");
const char *tls_key = str_map_find (&ctx->config, "tls_key");
// Only try to enable SSL support if the user configures it; it is not
// a failure if no one has requested it.
if (!tls_cert && !tls_key)
return true;
if (!tls_cert)
error_set (e, "no TLS certificate set");
else if (!tls_key)
error_set (e, "no TLS private key set");
if (!tls_cert || !tls_key)
return false;
bool result = false;
char *cert_path = resolve_filename
(tls_cert, resolve_relative_config_filename);
char *key_path = resolve_filename
(tls_key, resolve_relative_config_filename);
if (!cert_path)
error_set (e, "%s: %s", "cannot open file", tls_cert);
else if (!key_path)
error_set (e, "%s: %s", "cannot open file", tls_key);
else
result = irc_initialize_ssl_ctx (ctx, cert_path, key_path, e);
free (cert_path);
free (key_path);
return result;
}
static bool
irc_initialize_catalog (struct server_context *ctx, struct error **e)
{
hard_assert (ctx->catalog == (nl_catd) -1);
const char *catalog = str_map_find (&ctx->config, "catalog");
if (!catalog)
return true;
char *path = resolve_filename (catalog, resolve_relative_config_filename);
if (!path)
{
error_set (e, "%s: %s", "cannot open file", catalog);
return false;
}
ctx->catalog = catopen (path, NL_CAT_LOCALE);
free (path);
if (ctx->catalog == (nl_catd) -1)
{
error_set (e, "%s: %s",
"failed reading the message catalog file", strerror (errno));
return false;
}
return true;
}
static bool
irc_initialize_motd (struct server_context *ctx, struct error **e)
{
hard_assert (ctx->motd.len == 0);
const char *motd = str_map_find (&ctx->config, "motd");
if (!motd)
return true;
char *path = resolve_filename (motd, resolve_relative_config_filename);
if (!path)
{
error_set (e, "%s: %s", "cannot open file", motd);
return false;
}
FILE *fp = fopen (path, "r");
free (path);
if (!fp)
{
error_set (e, "%s: %s",
"failed reading the MOTD file", strerror (errno));
return false;
}
struct str line = str_make ();
while (read_line (fp, &line))
strv_append_owned (&ctx->motd, str_steal (&line));
str_free (&line);
fclose (fp);
return true;
}
static bool
irc_parse_config_unsigned (const char *name, const char *value, unsigned *out,
unsigned long min, unsigned long max, struct error **e)
{
unsigned long ul;
hard_assert (value != NULL);
if (!xstrtoul (&ul, value, 10) || ul > max || ul < min)
{
error_set (e, "invalid configuration value for `%s': %s",
name, "the number is invalid or out of range");
return false;
}
*out = ul;
return true;
}
/// This function handles values that require validation before their first use,
/// or some kind of a transformation (such as conversion to an integer) needs
/// to be done before they can be used directly.
static bool
irc_parse_config (struct server_context *ctx, struct error **e)
{
#define PARSE_UNSIGNED(name, min, max) \
irc_parse_config_unsigned (#name, str_map_find (&ctx->config, #name), \
&ctx->name, min, max, e)
if (!PARSE_UNSIGNED (ping_interval, 1, UINT_MAX)
|| !PARSE_UNSIGNED (max_connections, 0, UINT_MAX))
return false;
bool result = true;
struct strv fingerprints = strv_make ();
const char *operators = str_map_find (&ctx->config, "operators");
if (operators)
cstr_split (operators, ",", true, &fingerprints);
for (size_t i = 0; i < fingerprints.len; i++)
{
const char *key = fingerprints.vector[i];
if (!irc_is_valid_fingerprint (key))
{
error_set (e, "invalid configuration value for `%s': %s",
"operators", "invalid fingerprint value");
result = false;
break;
}
str_map_set (&ctx->operators, key, (void *) 1);
}
strv_free (&fingerprints);
return result;
}
static bool
irc_initialize_server_name (struct server_context *ctx, struct error **e)
{
enum validation_result res;
const char *server_name = str_map_find (&ctx->config, "server_name");
if (server_name)
{
res = irc_validate_hostname (server_name);
if (res != VALIDATION_OK)
{
error_set (e, "invalid configuration value for `%s': %s",
"server_name", irc_validate_to_str (res));
return false;
}
ctx->server_name = xstrdup (server_name);
}
else
{
long host_name_max = sysconf (_SC_HOST_NAME_MAX);
if (host_name_max <= 0)
host_name_max = _POSIX_HOST_NAME_MAX;
char hostname[host_name_max + 1];
if (gethostname (hostname, sizeof hostname))
{
error_set (e, "%s: %s",
"getting the hostname failed", strerror (errno));
return false;
}
res = irc_validate_hostname (hostname);
if (res != VALIDATION_OK)
{
error_set (e,
"`%s' is not set and the hostname (`%s') cannot be used: %s",
"server_name", hostname, irc_validate_to_str (res));
return false;
}
ctx->server_name = xstrdup (hostname);
}
return true;
}
static bool
irc_lock_pid_file (struct server_context *ctx, struct error **e)
{
const char *path = str_map_find (&ctx->config, "pid_file");
if (!path)
return true;
char *resolved = resolve_filename (path, resolve_relative_runtime_filename);
bool result = lock_pid_file (resolved, e) != -1;
free (resolved);
return result;
}
static int
irc_listen (struct addrinfo *gai_iter)
{
int fd = socket (gai_iter->ai_family,
gai_iter->ai_socktype, gai_iter->ai_protocol);
if (fd == -1)
return -1;
set_cloexec (fd);
int yes = 1;
soft_assert (setsockopt (fd, SOL_SOCKET, SO_KEEPALIVE,
&yes, sizeof yes) != -1);
soft_assert (setsockopt (fd, SOL_SOCKET, SO_REUSEADDR,
&yes, sizeof yes) != -1);
char host[NI_MAXHOST], port[NI_MAXSERV];
host[0] = port[0] = '\0';
int err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen,
host, sizeof host, port, sizeof port,
NI_NUMERICHOST | NI_NUMERICSERV);
if (err)
print_debug ("%s: %s", "getnameinfo", gai_strerror (err));
char *address = format_host_port_pair (host, port);
if (bind (fd, gai_iter->ai_addr, gai_iter->ai_addrlen))
print_error ("bind to %s failed: %s", address, strerror (errno));
else if (listen (fd, 16 /* arbitrary number */))
print_error ("listen on %s failed: %s", address, strerror (errno));
else
{
print_status ("listening on %s", address);
free (address);
return fd;
}
free (address);
xclose (fd);
return -1;
}
static void
irc_listen_resolve (struct server_context *ctx,
const char *host, const char *port, struct addrinfo *gai_hints)
{
struct addrinfo *gai_result, *gai_iter;
int err = getaddrinfo (host, port, gai_hints, &gai_result);
if (err)
{
char *address = format_host_port_pair (host, port);
print_error ("bind to %s failed: %s: %s",
address, "getaddrinfo", gai_strerror (err));
free (address);
return;
}
int fd;
for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next)
{
if ((fd = irc_listen (gai_iter)) == -1)
continue;
set_blocking (fd, false);
struct poller_fd *event = &ctx->listen_events[ctx->n_listen_fds];
*event = poller_fd_make (&ctx->poller, fd);
event->dispatcher = (poller_fd_fn) on_irc_client_available;
event->user_data = ctx;
ctx->listen_fds[ctx->n_listen_fds++] = fd;
poller_fd_set (event, POLLIN);
break;
}
freeaddrinfo (gai_result);
}
static bool
irc_setup_listen_fds (struct server_context *ctx, struct error **e)
{
const char *bind_host = str_map_find (&ctx->config, "bind_host");
const char *bind_port = str_map_find (&ctx->config, "bind_port");
hard_assert (bind_port != NULL); // We have a default value for this
struct addrinfo gai_hints;
memset (&gai_hints, 0, sizeof gai_hints);
gai_hints.ai_socktype = SOCK_STREAM;
gai_hints.ai_flags = AI_PASSIVE;
struct strv ports = strv_make ();
cstr_split (bind_port, ",", true, &ports);
ctx->listen_fds = xcalloc (ports.len, sizeof *ctx->listen_fds);
ctx->listen_events = xcalloc (ports.len, sizeof *ctx->listen_events);
for (size_t i = 0; i < ports.len; i++)
irc_listen_resolve (ctx, bind_host, ports.vector[i], &gai_hints);
strv_free (&ports);
if (!ctx->n_listen_fds)
{
error_set (e, "%s: %s",
"network setup failed", "no ports to listen on");
return false;
}
return true;
}
// --- Main --------------------------------------------------------------------
static void
on_signal_pipe_readable (const struct pollfd *fd, struct server_context *ctx)
{
char dummy;
(void) read (fd->fd, &dummy, 1);
if (g_termination_requested && !ctx->quitting)
irc_initiate_quit (ctx);
}
static void
daemonize (struct server_context *ctx)
{
print_status ("daemonizing...");
if (chdir ("/"))
exit_fatal ("%s: %s", "chdir", strerror (errno));
// Because of systemd, we need to exit the parent process _after_ writing
// a PID file, otherwise our grandchild would receive a SIGTERM
int sync_pipe[2];
if (pipe (sync_pipe))
exit_fatal ("%s: %s", "pipe", strerror (errno));
pid_t pid;
if ((pid = fork ()) < 0)
exit_fatal ("%s: %s", "fork", strerror (errno));
else if (pid)
{
// Wait until all write ends of the pipe are closed, which can mean
// either success or failure, we don't need to care
xclose (sync_pipe[PIPE_WRITE]);
char dummy;
if (read (sync_pipe[PIPE_READ], &dummy, 1) < 0)
exit_fatal ("%s: %s", "read", strerror (errno));
exit (EXIT_SUCCESS);
}
setsid ();
signal (SIGHUP, SIG_IGN);
if ((pid = fork ()) < 0)
exit_fatal ("%s: %s", "fork", strerror (errno));
else if (pid)
exit (EXIT_SUCCESS);
openlog (PROGRAM_NAME, LOG_NDELAY | LOG_NOWAIT | LOG_PID, 0);
g_log_message_real = log_message_syslog;
// Write the PID file (if so configured) and get rid of the pipe, so that
// the read() in our grandparent finally returns zero (no write ends)
struct error *e = NULL;
if (!irc_lock_pid_file (ctx, &e))
exit_fatal ("%s", e->message);
xclose (sync_pipe[PIPE_READ]);
xclose (sync_pipe[PIPE_WRITE]);
// XXX: we may close our own descriptors this way, crippling ourselves;
// there is no real guarantee that we will start with all three
// descriptors open. In theory we could try to enumerate the descriptors
// at the start of main().
for (int i = 0; i < 3; i++)
xclose (i);
int tty = open ("/dev/null", O_RDWR);
if (tty != 0 || dup (0) != 1 || dup (0) != 2)
exit_fatal ("failed to reopen FD's: %s", strerror (errno));
poller_post_fork (&ctx->poller);
}
int
main (int argc, char *argv[])
{
// Need to call this first as all string maps depend on it
siphash_wrapper_randomize ();
static const struct opt opts[] =
{
{ 'd', "debug", NULL, 0, "run in debug mode (do not daemonize)" },
{ '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_make (argc, argv, opts, NULL, "Experimental IRC daemon.");
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_simple_config_write_default (optarg, g_config_table);
exit (EXIT_SUCCESS);
default:
print_error ("wrong options");
opt_handler_usage (&oh, stderr);
exit (EXIT_FAILURE);
}
opt_handler_free (&oh);
print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting");
setup_signal_handlers ();
init_openssl ();
struct server_context ctx;
server_context_init (&ctx);
ctx.started = time (NULL);
irc_register_handlers (&ctx);
irc_register_cap_handlers (&ctx);
struct error *e = NULL;
if (!simple_config_update_from_file (&ctx.config, &e))
{
print_error ("error loading configuration: %s", e->message);
error_free (e);
exit (EXIT_FAILURE);
}
ctx.signal_event = poller_fd_make (&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);
if (!irc_initialize_tls (&ctx, &e)
|| !irc_initialize_server_name (&ctx, &e)
|| !irc_initialize_motd (&ctx, &e)
|| !irc_initialize_catalog (&ctx, &e)
|| !irc_parse_config (&ctx, &e)
|| !irc_setup_listen_fds (&ctx, &e))
exit_fatal ("%s", e->message);
if (!g_debug_mode)
daemonize (&ctx);
else if (!irc_lock_pid_file (&ctx, &e))
exit_fatal ("%s", e->message);
#if OpenBSD >= 201605
// This won't be as simple once we decide to implement REHASH
if (pledge ("stdio inet dns", NULL))
exit_fatal ("%s: %s", "pledge", strerror (errno));
#endif
ctx.polling = true;
while (ctx.polling)
poller_run (&ctx.poller);
server_context_free (&ctx);
return EXIT_SUCCESS;
}