Přemysl Janouch
24a082b5d8
First of all, we used to write the wrong PID in the file. Second of all, systemd needs some extra BS to not kill us immediately.
3993 lines
110 KiB
C
3993 lines
110 KiB
C
/*
|
|
* kike.c: the experimental IRC daemon
|
|
*
|
|
* Copyright (c) 2014 - 2015, Přemysl Janouch <p.janouch@gmail.com>
|
|
*
|
|
* Permission to use, copy, modify, and/or distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
|
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
|
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*
|
|
*/
|
|
|
|
#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) ------------------------------------
|
|
|
|
static struct 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" },
|
|
{ "ssl_cert", NULL, "Server SSL certificate (PEM)" },
|
|
{ "ssl_key", NULL, "Server SSL private key (PEM)" },
|
|
|
|
{ "operators", NULL, "IRCop SSL cert. fingerprints" },
|
|
|
|
{ "max_connections", "0", "Global connection limit" },
|
|
{ "ping_interval", "180", "Interval between PING's (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)
|
|
{
|
|
regex_cache_init (&cache);
|
|
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
|
|
#define SN "[0-9A-Za-z][-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;
|
|
}
|
|
|
|
static bool
|
|
irc_is_valid_host (const char *host)
|
|
{
|
|
return irc_validate_hostname (host) == VALIDATION_OK
|
|
|| irc_is_valid_hostaddr (host);
|
|
}
|
|
|
|
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)
|
|
};
|
|
|
|
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
|
|
|
|
bool initialized; ///< Has any data been received yet?
|
|
bool cap_negotiating; ///< Negotiating capabilities
|
|
bool registered; ///< The user has registered
|
|
bool closing_link; ///< Closing link
|
|
bool half_closed; ///< Closing link: conn. is half-closed
|
|
|
|
unsigned long cap_version; ///< CAP protocol version
|
|
unsigned caps_enabled; ///< Enabled capabilities
|
|
|
|
bool ssl_rx_want_tx; ///< SSL_read() wants to write
|
|
bool ssl_tx_want_rx; ///< 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 *address; ///< Full address including port
|
|
|
|
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
|
|
};
|
|
|
|
static void
|
|
client_init (struct client *self)
|
|
{
|
|
memset (self, 0, sizeof *self);
|
|
|
|
self->socket_fd = -1;
|
|
str_init (&self->read_buffer);
|
|
str_init (&self->write_buffer);
|
|
self->cap_version = 301;
|
|
// TODO: make this configurable and more fine-grained
|
|
flood_detector_init (&self->antiflood, 10, 20);
|
|
str_map_init (&self->invites);
|
|
self->invites.key_xfrm = irc_strxfrm;
|
|
}
|
|
|
|
static void
|
|
client_free (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->nickname);
|
|
free (self->username);
|
|
free (self->realname);
|
|
|
|
free (self->hostname);
|
|
free (self->address);
|
|
free (self->away_message);
|
|
flood_detector_free (&self->antiflood);
|
|
str_map_free (&self->invites);
|
|
}
|
|
|
|
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 str_vector ban_list; ///< Ban list
|
|
struct str_vector exception_list; ///< Exceptions from bans
|
|
struct str_vector 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 ("");
|
|
|
|
str_vector_init (&self->ban_list);
|
|
str_vector_init (&self->exception_list);
|
|
str_vector_init (&self->invite_list);
|
|
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);
|
|
}
|
|
|
|
str_vector_free (&self->ban_list);
|
|
str_vector_free (&self->exception_list);
|
|
str_vector_free (&self->invite_list);
|
|
|
|
free (self);
|
|
}
|
|
|
|
static char *
|
|
channel_get_mode (struct channel *self, bool disclose_secrets)
|
|
{
|
|
struct str mode;
|
|
str_init (&mode);
|
|
|
|
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 str_vector motd; ///< MOTD (none if empty)
|
|
nl_catd catalog; ///< Message catalog for server msgs
|
|
struct str_map operators; ///< SSL 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);
|
|
|
|
str_map_init (&self->users);
|
|
self->users.key_xfrm = irc_strxfrm;
|
|
str_map_init (&self->channels);
|
|
self->channels.key_xfrm = irc_strxfrm;
|
|
self->channels.free = (void (*) (void *)) channel_delete;
|
|
str_map_init (&self->handlers);
|
|
self->handlers.key_xfrm = irc_strxfrm;
|
|
str_map_init (&self->cap_handlers);
|
|
self->cap_handlers.key_xfrm = irc_strxfrm;
|
|
|
|
str_map_init (&self->whowas);
|
|
self->whowas.key_xfrm = irc_strxfrm;
|
|
self->whowas.free = (void (*) (void *)) whowas_info_destroy;
|
|
|
|
poller_init (&self->poller);
|
|
poller_timer_init (&self->quit_timer, &self->poller);
|
|
self->quit_timer.dispatcher = on_irc_quit_timeout;
|
|
self->quit_timer.user_data = self;
|
|
|
|
str_map_init (&self->config);
|
|
self->config.free = free;
|
|
load_config_defaults (&self->config, g_config_table);
|
|
|
|
str_vector_init (&self->motd);
|
|
self->catalog = (nl_catd) -1;
|
|
str_map_init (&self->operators);
|
|
// 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++)
|
|
{
|
|
xclose (self->listen_fds[i]);
|
|
self->listen_events[i].closed = true;
|
|
poller_fd_reset (&self->listen_events[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);
|
|
|
|
str_vector_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++)
|
|
{
|
|
xclose (ctx->listen_fds[i]);
|
|
ctx->listen_events[i].closed = true;
|
|
poller_fd_reset (&ctx->listen_events[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_init (&targets);
|
|
targets.key_xfrm = irc_strxfrm;
|
|
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, &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);
|
|
}
|
|
|
|
str_map_iter_init (&iter, &targets);
|
|
struct client *target;
|
|
while ((target = str_map_iter_next (&iter)))
|
|
if (target != c)
|
|
client_send (target, "%s", message);
|
|
}
|
|
|
|
// --- 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_init (&mode);
|
|
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->initialized && !c->closing_link);
|
|
|
|
size_t old_sendq = c->write_buffer.len;
|
|
// TODO: kill the connection above some "SendQ" threshold (careful!)
|
|
str_append_data (&c->write_buffer, s->str,
|
|
s->len > IRC_MAX_MESSAGE_LENGTH ? IRC_MAX_MESSAGE_LENGTH : s->len);
|
|
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_init (&tmp);
|
|
|
|
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_iter iter;
|
|
str_map_iter_init (&iter, &c->ctx->channels);
|
|
struct channel *chan, *next = str_map_iter_next (&iter);
|
|
for (chan = next; chan; chan = next)
|
|
{
|
|
next = str_map_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);
|
|
}
|
|
|
|
client_add_to_whowas (c);
|
|
|
|
str_map_set (&c->ctx->users, c->nickname, NULL);
|
|
free (c->nickname);
|
|
c->nickname = NULL;
|
|
c->registered = false;
|
|
}
|
|
|
|
static void
|
|
client_close_link (struct client *c, const char *reason)
|
|
{
|
|
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 void
|
|
client_kill (struct client *c, const char *reason)
|
|
{
|
|
client_unregister (c, reason ? reason : "Client exited");
|
|
|
|
struct server_context *ctx = c->ctx;
|
|
|
|
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_event.closed = true;
|
|
poller_fd_reset (&c->socket_event);
|
|
client_cancel_timers (c);
|
|
|
|
print_debug ("closed connection to %s (%s)",
|
|
c->address, reason ? reason : "");
|
|
|
|
c->socket_fd = -1;
|
|
client_free (c);
|
|
LIST_UNLINK (ctx->clients, c);
|
|
ctx->n_clients--;
|
|
free (c);
|
|
|
|
irc_try_finish_quit (ctx);
|
|
}
|
|
|
|
static bool
|
|
client_in_mask_list (const struct client *c, const struct str_vector *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_init (&fingerprint);
|
|
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);
|
|
}
|
|
|
|
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 (void *user_data)
|
|
{
|
|
struct client *c = user_data;
|
|
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 (void *user_data)
|
|
{
|
|
struct client *c = user_data;
|
|
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 (void *user_data)
|
|
{
|
|
struct client *c = user_data;
|
|
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_init (&reply);
|
|
|
|
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_init (&common);
|
|
|
|
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_init (&reply);
|
|
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_init (&iter, &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 SSL 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 str_vector 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" },
|
|
};
|
|
|
|
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", c->ctx->server_name, a->target);
|
|
}
|
|
|
|
static void
|
|
irc_handle_cap_list (struct client *c, struct irc_cap_args *a)
|
|
{
|
|
struct str_vector caps;
|
|
str_vector_init (&caps);
|
|
|
|
for (size_t i = 0; i < N_ELEMENTS (irc_cap_table); i++)
|
|
if (c->caps_enabled & irc_cap_table[i].flag)
|
|
str_vector_add (&caps, irc_cap_table[i].name);
|
|
|
|
char *caps_str = join_str_vector (&caps, ' ');
|
|
str_vector_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 = "";
|
|
str_vector_init (&args.params);
|
|
|
|
if (msg->params.len > 1)
|
|
{
|
|
args.full_params = msg->params.vector[1];
|
|
split_str_ignore_empty (args.full_params, ' ', &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);
|
|
|
|
str_vector_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 SSL 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);
|
|
|
|
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);
|
|
free (c->nickname);
|
|
}
|
|
c->nickname = xstrdup (nickname);
|
|
str_map_set (&ctx->users, nickname, c);
|
|
|
|
if (!c->registered)
|
|
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";
|
|
|
|
free (c->username);
|
|
c->username = xstrdup (username);
|
|
free (c->realname);
|
|
c->realname = xstrdup (realname);
|
|
|
|
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_init (&reply);
|
|
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_init (&diff);
|
|
|
|
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 SSL"
|
|
" 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 str_vector *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_init (&result);
|
|
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 str_vector added_params; ///< Params for added modes
|
|
struct str_vector removed_params; ///< Params for removed modes
|
|
|
|
struct str *output; ///< "added" or "removed"
|
|
struct str_vector *output_params; ///< Similarly for "*_params"
|
|
};
|
|
|
|
static void
|
|
mode_processor_init (struct mode_processor *self)
|
|
{
|
|
memset (self, 0, sizeof *self);
|
|
|
|
str_init (&self->added);
|
|
str_init (&self->removed);
|
|
|
|
str_vector_init (&self->added_params);
|
|
str_vector_init (&self->removed_params);
|
|
}
|
|
|
|
static void
|
|
mode_processor_free (struct mode_processor *self)
|
|
{
|
|
str_free (&self->added);
|
|
str_free (&self->removed);
|
|
|
|
str_vector_free (&self->added_params);
|
|
str_vector_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); \
|
|
str_vector_add (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 str_vector *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;
|
|
}
|
|
|
|
char *mask = irc_check_expand_user_mask (target);
|
|
if (!mode_processor_check_operator (self) || !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)
|
|
str_vector_add (list, mask);
|
|
else
|
|
str_vector_remove (list, i);
|
|
|
|
str_append_c (self->output, self->mode_char);
|
|
str_vector_add (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);
|
|
str_vector_add (&self->removed_params, self->channel->key);
|
|
free (self->channel->key);
|
|
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);
|
|
str_vector_add (&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);
|
|
str_vector_add (&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_init (&p);
|
|
|
|
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++))
|
|
break;
|
|
}
|
|
|
|
// TODO: limit to three changes with parameter per command
|
|
if (p.added.len || p.removed.len)
|
|
{
|
|
struct str message;
|
|
str_init (&message);
|
|
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)
|
|
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_init (&iter, &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 str_vector channels;
|
|
str_vector_init (&channels);
|
|
split_str_ignore_empty (msg->params.vector[0], ',', &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);
|
|
str_vector_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_init (&prefixes);
|
|
|
|
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_init (&result);
|
|
|
|
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)
|
|
{
|
|
struct str_vector nicks;
|
|
str_vector_init (&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);
|
|
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);
|
|
str_vector_add_owned (&nicks,
|
|
irc_make_rpl_namreply_item (c, iter->c, iter));
|
|
}
|
|
|
|
irc_send_reply_vector (c, IRC_RPL_NAMREPLY,
|
|
nicks.vector, type, chan->name, "");
|
|
str_vector_free (&nicks);
|
|
}
|
|
|
|
static void
|
|
irc_send_disassociated_names (struct client *c, struct str_map *used)
|
|
{
|
|
struct str_vector nicks;
|
|
str_vector_init (&nicks);
|
|
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, &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;
|
|
str_vector_add_owned (&nicks,
|
|
irc_make_rpl_namreply_item (c, target, NULL));
|
|
}
|
|
|
|
if (nicks.len)
|
|
irc_send_reply_vector (c, IRC_RPL_NAMREPLY,
|
|
nicks.vector, '*', "*", "");
|
|
str_vector_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_init (&used);
|
|
used.key_xfrm = irc_strxfrm;
|
|
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, &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 str_vector channels;
|
|
str_vector_init (&channels);
|
|
split_str_ignore_empty (msg->params.vector[0], ',', &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]);
|
|
}
|
|
str_vector_free (&channels);
|
|
}
|
|
}
|
|
|
|
static void
|
|
irc_send_rpl_whoreply (struct client *c, const struct channel *chan,
|
|
const struct client *target)
|
|
{
|
|
struct str chars;
|
|
str_init (&chars);
|
|
|
|
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_init (&iter, &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;
|
|
str_map_iter_init (&iter, &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_init (&iter, &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 str_vector channels;
|
|
str_vector_init (&channels);
|
|
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, &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_init (&item);
|
|
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);
|
|
str_vector_add_owned (&channels, str_steal (&item));
|
|
}
|
|
|
|
irc_send_reply_vector (c, IRC_RPL_WHOISCHANNELS, channels.vector, nick, "");
|
|
str_vector_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 str_vector masks;
|
|
str_vector_init (&masks);
|
|
const char *masks_str = msg->params.vector[msg->params.len > 1];
|
|
split_str_ignore_empty (masks_str, ',', &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_init (&iter, &c->ctx->users);
|
|
bool found = false;
|
|
while ((target = str_map_iter_next (&iter))
|
|
&& !irc_fnmatch (mask, target->nickname))
|
|
{
|
|
irc_send_whois_reply (c, target);
|
|
found = true;
|
|
}
|
|
if (!found)
|
|
irc_send_reply (c, IRC_ERR_NOSUCHNICK, mask);
|
|
}
|
|
}
|
|
str_vector_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 str_vector nicks;
|
|
str_vector_init (&nicks);
|
|
split_str_ignore_empty (msg->params.vector[0], ',', &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);
|
|
}
|
|
str_vector_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);
|
|
|
|
free (chan->topic);
|
|
free (chan->topic_who);
|
|
chan->topic = xstrdup (msg->params.vector[1]);
|
|
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)
|
|
{
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, &c->ctx->channels);
|
|
struct channel *chan, *next = str_map_iter_next (&iter);
|
|
for (chan = next; chan; chan = next)
|
|
{
|
|
// We have to be careful here, the channel might get destroyed
|
|
next = str_map_iter_next (&iter);
|
|
if (channel_get_user (chan, c))
|
|
irc_try_part (c, chan->name, NULL);
|
|
}
|
|
}
|
|
|
|
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 str_vector channels;
|
|
str_vector_init (&channels);
|
|
split_str_ignore_empty (msg->params.vector[0], ',', &channels);
|
|
for (size_t i = 0; i < channels.len; i++)
|
|
irc_try_part (c, channels.vector[i], reason);
|
|
str_vector_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 str_vector channels;
|
|
struct str_vector users;
|
|
str_vector_init (&channels);
|
|
str_vector_init (&users);
|
|
split_str_ignore_empty (msg->params.vector[0], ',', &channels);
|
|
split_str_ignore_empty (msg->params.vector[1], ',', &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);
|
|
|
|
str_vector_free (&channels);
|
|
str_vector_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 str_vector channels;
|
|
struct str_vector keys;
|
|
str_vector_init (&channels);
|
|
str_vector_init (&keys);
|
|
split_str_ignore_empty (msg->params.vector[0], ',', &channels);
|
|
if (msg->params.len > 1)
|
|
split_str_ignore_empty (msg->params.vector[1], ',', &keys);
|
|
|
|
for (size_t i = 0; i < channels.len; i++)
|
|
irc_try_join (c, channels.vector[i],
|
|
i < keys.len ? keys.vector[i] : NULL);
|
|
|
|
str_vector_free (&channels);
|
|
str_vector_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)
|
|
{
|
|
free (c->away_message);
|
|
c->away_message = NULL;
|
|
irc_send_reply (c, IRC_RPL_UNAWAY);
|
|
}
|
|
else
|
|
{
|
|
free (c->away_message);
|
|
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_init (&result);
|
|
|
|
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_init (&iter, &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;
|
|
if (msg->params.len < 1 || !(query = *msg->params.vector[0]))
|
|
RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
|
|
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);
|
|
}
|
|
|
|
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]);
|
|
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)
|
|
{
|
|
(void) raw;
|
|
|
|
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_ensure_space (buf, 512);
|
|
n_read = recv (c->socket_fd, buf->str + buf->len,
|
|
buf->alloc - buf->len - 1 /* null byte */, 0);
|
|
|
|
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__, "recv", strerror (errno));
|
|
client_kill (c, strerror (errno));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
irc_try_read_ssl (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_ensure_space (buf, 512);
|
|
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 = send (c->socket_fd, buf->str, buf->len, 0);
|
|
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__, "send", strerror (errno));
|
|
client_kill (c, strerror (errno));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
irc_try_write_ssl (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)
|
|
{
|
|
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_ssl (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;
|
|
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_ssl (struct client *c)
|
|
{
|
|
const char *error_info = NULL;
|
|
if (!c->ctx->ssl_ctx)
|
|
{
|
|
error_info = "SSL support disabled";
|
|
goto error_ssl_1;
|
|
}
|
|
|
|
c->ssl = SSL_new (c->ctx->ssl_ctx);
|
|
if (!c->ssl)
|
|
goto error_ssl_1;
|
|
if (!SSL_set_fd (c->ssl, c->socket_fd))
|
|
goto error_ssl_2;
|
|
|
|
SSL_set_accept_state (c->ssl);
|
|
return true;
|
|
|
|
error_ssl_2:
|
|
SSL_free (c->ssl);
|
|
c->ssl = NULL;
|
|
error_ssl_1:
|
|
// XXX: these error strings are really nasty; also there could be
|
|
// multiple errors on the OpenSSL stack.
|
|
if (!error_info)
|
|
error_info = ERR_error_string (ERR_get_error (), NULL);
|
|
print_debug ("could not initialize SSL 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_ssl (c) && !client_initialize_ssl (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_ssl (c) || !irc_try_write_ssl (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)
|
|
{
|
|
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->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 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)
|
|
return false;
|
|
if (errno == EINTR
|
|
|| errno == ECONNABORTED)
|
|
return true;
|
|
|
|
// TODO: handle resource exhaustion (EMFILE, ENFILE) specially
|
|
// (stop accepting new connections and wait until we close some;
|
|
// also set a timer in case of ENFILE).
|
|
print_fatal ("%s: %s", "accept", strerror (errno));
|
|
irc_initiate_quit (ctx);
|
|
return false;
|
|
}
|
|
|
|
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_NUMERICSERV);
|
|
if (err)
|
|
print_debug ("%s: %s", "getnameinfo", gai_strerror (err));
|
|
|
|
char *address = format_host_port_pair (host, port);
|
|
print_debug ("accepted connection from %s", address);
|
|
|
|
struct client *c = xmalloc (sizeof *c);
|
|
client_init (c);
|
|
c->ctx = ctx;
|
|
c->opened = time (NULL);
|
|
c->socket_fd = fd;
|
|
c->hostname = xstrdup (host);
|
|
c->address = address;
|
|
c->last_active = time (NULL);
|
|
LIST_PREPEND (ctx->clients, c);
|
|
ctx->n_clients++;
|
|
|
|
poller_fd_init (&c->socket_event, &c->ctx->poller, c->socket_fd);
|
|
c->socket_event.dispatcher = (poller_fd_fn) on_client_ready;
|
|
c->socket_event.user_data = c;
|
|
|
|
poller_timer_init (&c->kill_timer, &c->ctx->poller);
|
|
c->kill_timer.dispatcher = on_client_kill_timer;
|
|
c->kill_timer.user_data = c;
|
|
|
|
poller_timer_init (&c->timeout_timer, &c->ctx->poller);
|
|
c->timeout_timer.dispatcher = on_client_timeout_timer;
|
|
c->timeout_timer.user_data = c;
|
|
|
|
poller_timer_init (&c->ping_timer, &c->ctx->poller);
|
|
c->ping_timer.dispatcher = on_client_ping_timer;
|
|
c->ping_timer.user_data = c;
|
|
|
|
set_blocking (fd, false);
|
|
client_update_poller (c, NULL);
|
|
client_set_kill_timer (c);
|
|
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;
|
|
|
|
// We only want to provide additional privileges based on the client's
|
|
// certificate, so let's not terminate the connection because of a failure.
|
|
return 1;
|
|
}
|
|
|
|
static bool
|
|
irc_initialize_ssl_ctx (struct server_context *ctx,
|
|
const char *cert_path, const char *key_path, struct error **e)
|
|
{
|
|
ctx->ssl_ctx = SSL_CTX_new (SSLv23_server_method ());
|
|
if (!ctx->ssl_ctx)
|
|
{
|
|
// XXX: these error strings are really nasty; also there could be
|
|
// multiple errors on the OpenSSL stack.
|
|
error_set (e, "%s: %s", "could not initialize SSL",
|
|
ERR_error_string (ERR_get_error (), NULL));
|
|
return false;
|
|
}
|
|
SSL_CTX_set_verify (ctx->ssl_ctx,
|
|
SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, irc_ssl_verify_callback);
|
|
// XXX: maybe we should call SSL_CTX_set_options() for some workarounds
|
|
|
|
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);
|
|
|
|
// Gah, spare me your awkward semantics, I just want to push data!
|
|
// XXX: do we want SSL_MODE_AUTO_RETRY as well? I guess not.
|
|
SSL_CTX_set_mode (ctx->ssl_ctx,
|
|
SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | SSL_MODE_ENABLE_PARTIAL_WRITE);
|
|
|
|
// XXX: perhaps we should read the files ourselves for better messages
|
|
if (!SSL_CTX_use_certificate_chain_file (ctx->ssl_ctx, cert_path))
|
|
error_set (e, "%s: %s", "setting the SSL client certificate failed",
|
|
ERR_error_string (ERR_get_error (), NULL));
|
|
else if (!SSL_CTX_use_PrivateKey_file
|
|
(ctx->ssl_ctx, key_path, SSL_FILETYPE_PEM))
|
|
error_set (e, "%s: %s", "setting the SSL private key failed",
|
|
ERR_error_string (ERR_get_error (), NULL));
|
|
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_ssl (struct server_context *ctx, struct error **e)
|
|
{
|
|
const char *ssl_cert = str_map_find (&ctx->config, "ssl_cert");
|
|
const char *ssl_key = str_map_find (&ctx->config, "ssl_key");
|
|
|
|
// Only try to enable SSL support if the user configures it; it is not
|
|
// a failure if no one has requested it.
|
|
if (!ssl_cert && !ssl_key)
|
|
return true;
|
|
|
|
if (!ssl_cert)
|
|
error_set (e, "no SSL certificate set");
|
|
else if (!ssl_key)
|
|
error_set (e, "no SSL private key set");
|
|
if (!ssl_cert || !ssl_key)
|
|
return false;
|
|
|
|
bool result = false;
|
|
|
|
char *cert_path = resolve_config_filename (ssl_cert);
|
|
char *key_path = resolve_config_filename (ssl_key);
|
|
if (!cert_path)
|
|
error_set (e, "%s: %s", "cannot open file", ssl_cert);
|
|
else if (!key_path)
|
|
error_set (e, "%s: %s", "cannot open file", ssl_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_config_filename (catalog);
|
|
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_config_filename (motd);
|
|
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_init (&line);
|
|
while (read_line (fp, &line))
|
|
str_vector_add_owned (&ctx->motd, str_steal (&line));
|
|
str_free (&line);
|
|
|
|
fclose (fp);
|
|
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)
|
|
{
|
|
unsigned long ul;
|
|
#define PARSE_UNSIGNED(name, min, max) \
|
|
const char *name = str_map_find (&ctx->config, #name); \
|
|
hard_assert (name != NULL); \
|
|
if (!xstrtoul (&ul, name, 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; \
|
|
} \
|
|
ctx->name = ul
|
|
|
|
PARSE_UNSIGNED (ping_interval, 1, UINT_MAX);
|
|
PARSE_UNSIGNED (max_connections, 0, UINT_MAX);
|
|
|
|
bool result = true;
|
|
struct str_vector fingerprints;
|
|
str_vector_init (&fingerprints);
|
|
const char *operators = str_map_find (&ctx->config, "operators");
|
|
if (operators)
|
|
split_str_ignore_empty (operators, ',', &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);
|
|
}
|
|
str_vector_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
|
|
lock_pid_file (const char *path, struct error **e)
|
|
{
|
|
// When using XDG_RUNTIME_DIR, the file needs to either have its
|
|
// access time bumped every 6 hours, or have the sticky bit set
|
|
int fd = open (path, O_RDWR | O_CREAT,
|
|
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH /* 644 */ | S_ISVTX /* sticky */);
|
|
if (fd < 0)
|
|
{
|
|
error_set (e, "can't open `%s': %s", path, strerror (errno));
|
|
return false;
|
|
}
|
|
|
|
struct flock lock =
|
|
{
|
|
.l_type = F_WRLCK,
|
|
.l_start = 0,
|
|
.l_whence = SEEK_SET,
|
|
.l_len = 0,
|
|
};
|
|
if (fcntl (fd, F_SETLK, &lock))
|
|
{
|
|
error_set (e, "can't lock `%s': %s", path, strerror (errno));
|
|
xclose (fd);
|
|
return false;
|
|
}
|
|
|
|
struct str pid;
|
|
str_init (&pid);
|
|
str_append_printf (&pid, "%ld", (long) getpid ());
|
|
|
|
if (ftruncate (fd, 0)
|
|
|| write (fd, pid.str, pid.len) != (ssize_t) pid.len)
|
|
{
|
|
error_set (e, "can't write to `%s': %s", path, strerror (errno));
|
|
xclose (fd);
|
|
return false;
|
|
}
|
|
str_free (&pid);
|
|
|
|
// Intentionally not closing the file descriptor; it must stay alive
|
|
// for the entire life of the application
|
|
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);
|
|
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];
|
|
poller_fd_init (event, &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 str_vector ports;
|
|
str_vector_init (&ports);
|
|
split_str_ignore_empty (bind_port, ',', &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);
|
|
str_vector_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));
|
|
}
|
|
|
|
int
|
|
main (int argc, char *argv[])
|
|
{
|
|
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_init (&oh, 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_write_default_config (optarg, g_config_table);
|
|
exit (EXIT_SUCCESS);
|
|
default:
|
|
print_error ("wrong options");
|
|
opt_handler_usage (&oh, stderr);
|
|
exit (EXIT_FAILURE);
|
|
}
|
|
|
|
opt_handler_free (&oh);
|
|
|
|
print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting");
|
|
setup_signal_handlers ();
|
|
|
|
SSL_library_init ();
|
|
atexit (EVP_cleanup);
|
|
SSL_load_error_strings ();
|
|
// XXX: ERR_load_BIO_strings()? Anything else?
|
|
atexit (ERR_free_strings);
|
|
|
|
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 (!read_config_file (&ctx.config, &e))
|
|
{
|
|
print_error ("error loading configuration: %s", e->message);
|
|
error_free (e);
|
|
exit (EXIT_FAILURE);
|
|
}
|
|
|
|
poller_fd_init (&ctx.signal_event, &ctx.poller, g_signal_pipe[0]);
|
|
ctx.signal_event.dispatcher = (poller_fd_fn) on_signal_pipe_readable;
|
|
ctx.signal_event.user_data = &ctx;
|
|
poller_fd_set (&ctx.signal_event, POLLIN);
|
|
|
|
if (!irc_initialize_ssl (&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);
|
|
|
|
ctx.polling = true;
|
|
while (ctx.polling)
|
|
poller_run (&ctx.poller);
|
|
|
|
server_context_free (&ctx);
|
|
return EXIT_SUCCESS;
|
|
}
|