Přemysl Eric Janouch
383f6af344
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.
4062 lines
113 KiB
C
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;
|
|
}
|