diff options
-rw-r--r-- | src/common.c | 29 | ||||
-rw-r--r-- | src/kike.c | 784 |
2 files changed, 710 insertions, 103 deletions
diff --git a/src/common.c b/src/common.c index 5a505b2..389120c 100644 --- a/src/common.c +++ b/src/common.c @@ -642,7 +642,7 @@ str_map_iter_next (struct str_map_iter *self) self->link = self->link->next; while (!self->link) { - if (self->next_index >= map->len) + if (self->next_index >= map->alloc) return NULL; self->link = map->map[self->next_index++]; } @@ -1376,6 +1376,33 @@ strip_str_in_place (char *s, const char *stripped_chars) return s; } +static char * +join_str_vector (const struct str_vector *v, char delimiter) +{ + if (!v->len) + return xstrdup (""); + + struct str result; + str_init (&result); + str_append (&result, v->vector[0]); + for (size_t i = 1; i < v->len; i++) + str_append_printf (&result, "%c%s", delimiter, v->vector[i]); + return str_steal (&result); +} + +ATTRIBUTE_PRINTF (1, 2) +static char * +xstrdup_printf (const char *format, ...) +{ + va_list ap; + struct str tmp; + str_init (&tmp); + va_start (ap, format); + str_append_vprintf (&tmp, format, ap); + va_end (ap); + return str_steal (&tmp); +} + static bool str_append_env_path (struct str *output, const char *var, bool only_absolute) { @@ -103,6 +103,7 @@ enum validation_result // 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 @@ -194,6 +195,25 @@ irc_validate_nickname (const char *nickname) 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 @@ -284,12 +304,7 @@ client_get_mode (struct client *self) if (m & IRC_USER_MODE_RESTRICTED) str_append_c (&mode, 'r'); if (m & IRC_USER_MODE_OPERATOR) str_append_c (&mode, 'o'); if (m & IRC_USER_MODE_RX_SERVER_NOTICES) str_append_c (&mode, 's'); - - if (mode.len) - return str_steal (&mode); - - str_free (&mode); - return NULL; + return str_steal (&mode); } #define IRC_SUPPORTED_CHAN_MODES "ov" "imnqpst" "kl" @@ -339,6 +354,9 @@ channel_init (struct channel *self) { memset (self, 0, 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); @@ -363,6 +381,33 @@ channel_free (struct channel *self) str_vector_free (&self->invite_list); } +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'); + + if (self->user_limit != -1) + str_append_printf (&mode, " %ld", self->user_limit); + + // XXX: is this correct? Try it on an existing implementation. + if (self->key && disclose_secrets) + str_append_printf (&mode, " %s", self->key); + return str_steal (&mode); +} + struct server_context { int listen_fd; ///< Listening socket FD @@ -450,22 +495,7 @@ server_context_free (struct server_context *self) 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 *); - -static void -client_unregister (struct client *c, const char *reason) -{ - if (!c->registered) - return; - - // TODO: multicast a `:prefix QUIT :reason' to all people present on all - // channels we were on. - // TODO: remove ourselves from the channels, ... - str_map_set (&c->ctx->users, c->nickname, NULL); - free (c->nickname); - c->nickname = NULL; - - c->registered = false; -} +static void client_unregister (struct client *, const char *); static void irc_try_finish_quit (struct server_context *ctx) @@ -500,6 +530,8 @@ client_kill (struct client *c, const char *reason) static void irc_send_str (struct client *c, const struct str *s) { + hard_assert (c->initialized && !c->closing_link); + // 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); @@ -591,12 +623,10 @@ static void on_irc_client_timeout_timer (void *user_data) { struct client *c = user_data; - struct str reason; - str_init (&reason); - str_append_printf (&reason, "Ping timeout: >%u seconds", - c->ctx->ping_interval); - irc_close_link (c, reason.str); - str_free (&reason); + char *reason = xstrdup_printf + ("Ping timeout: >%u seconds", c->ctx->ping_interval); + irc_close_link (c, reason); + free (reason); } static void @@ -623,16 +653,31 @@ enum IRC_RPL_CREATED = 3, IRC_RPL_MYINFO = 4, + IRC_RPL_UMODEIS = 221, IRC_RPL_LUSERCLIENT = 251, IRC_RPL_LUSEROP = 252, IRC_RPL_LUSERUNKNOWN = 253, IRC_RPL_LUSERCHANNELS = 254, IRC_RPL_LUSERME = 255, + IRC_RPL_AWAY = 301, IRC_RPL_USERHOST = 302, + IRC_RPL_UNAWAY = 305, + IRC_RPL_NOWAWAY = 306, IRC_RPL_LIST = 322, IRC_RPL_LISTEND = 323, + IRC_RPL_CHANNELMODEIS = 324, + IRC_RPL_NOTOPIC = 331, + IRC_RPL_TOPIC = 332, + IRC_RPL_INVITELIST = 346, + IRC_RPL_ENDOFINVITELIST = 347, + IRC_RPL_EXCEPTLIST = 348, + IRC_RPL_ENDOFEXCEPTLIST = 349, IRC_RPL_VERSION = 351, + IRC_RPL_NAMREPLY = 353, + IRC_RPL_ENDOFNAMES = 366, + IRC_RPL_BANLIST = 367, + IRC_RPL_ENDOFBANLIST = 368, IRC_RPL_MOTD = 372, IRC_RPL_MOTDSTART = 375, IRC_RPL_ENDOFMOTD = 376, @@ -640,6 +685,8 @@ enum IRC_ERR_NOSUCHNICK = 401, IRC_ERR_NOSUCHSERVER = 402, + IRC_ERR_NOSUCHCHANNEL = 403, + IRC_ERR_CANNOTSENDTOCHAN = 404, IRC_ERR_NOORIGIN = 409, IRC_ERR_NORECIPIENT = 411, IRC_ERR_NOTEXTTOSEND = 412, @@ -648,9 +695,20 @@ enum IRC_ERR_NONICKNAMEGIVEN = 431, IRC_ERR_ERRONEOUSNICKNAME = 432, IRC_ERR_NICKNAMEINUSE = 433, + IRC_ERR_NOTONCHANNEL = 442, + IRC_ERR_SUMMONDISABLED = 445, + IRC_ERR_USERSDISABLED = 446, IRC_ERR_NOTREGISTERED = 451, IRC_ERR_NEEDMOREPARAMS = 461, - IRC_ERR_ALREADYREGISTERED = 462 + IRC_ERR_ALREADYREGISTERED = 462, + IRC_ERR_CHANNELISFULL = 471, + IRC_ERR_INVITEONLYCHAN = 473, + IRC_ERR_BANNEDFROMCHAN = 474, + IRC_ERR_BADCHANNELKEY = 475, + IRC_ERR_BADCHANMASK = 476, + IRC_ERR_CHANOPRIVSNEEDED = 482, + + IRC_ERR_USERSDONTMATCH = 502 }; static const char *g_default_replies[] = @@ -660,16 +718,31 @@ static const char *g_default_replies[] = [IRC_RPL_CREATED] = ":This server was created %s", [IRC_RPL_MYINFO] = "%s %s %s %s", + [IRC_RPL_UMODEIS] = "+%s", [IRC_RPL_LUSERCLIENT] = ":There are %d users and %d services on %d servers", [IRC_RPL_LUSEROP] = "%d :operator(s) online", [IRC_RPL_LUSERUNKNOWN] = "%d :unknown connection(s)", [IRC_RPL_LUSERCHANNELS] = "%d :channels formed", [IRC_RPL_LUSERME] = ":I have %d clients and %d servers", + [IRC_RPL_AWAY] = "%s :%s", [IRC_RPL_USERHOST] = ":%s", + [IRC_RPL_UNAWAY] = ":You are no longer marked as being away", + [IRC_RPL_NOWAWAY] = ":You have been marked as being away", [IRC_RPL_LIST] = "%s %d :%s", [IRC_RPL_LISTEND] = ":End of LIST", + [IRC_RPL_CHANNELMODEIS] = "%s +%s", + [IRC_RPL_NOTOPIC] = "%s :No topic is set", + [IRC_RPL_TOPIC] = "%s :%s", + [IRC_RPL_INVITELIST] = "%s %s", + [IRC_RPL_ENDOFINVITELIST] = "%s :End of channel invite list", + [IRC_RPL_EXCEPTLIST] = "%s %s", + [IRC_RPL_ENDOFEXCEPTLIST] = "%s :End of channel exception list", [IRC_RPL_VERSION] = "%s.%d %s :%s", + [IRC_RPL_NAMREPLY] = "%c %s :%s", + [IRC_RPL_ENDOFNAMES] = "%s :End of NAMES list", + [IRC_RPL_BANLIST] = "%s %s", + [IRC_RPL_ENDOFBANLIST] = "%s :End of channel ban list", [IRC_RPL_MOTD] = ":- %s", [IRC_RPL_MOTDSTART] = ":- %s Message of the day - ", [IRC_RPL_ENDOFMOTD] = ":End of MOTD command", @@ -677,6 +750,8 @@ static const char *g_default_replies[] = [IRC_ERR_NOSUCHNICK] = "%s :No such nick/channel", [IRC_ERR_NOSUCHSERVER] = "%s :No such server", + [IRC_ERR_NOSUCHCHANNEL] = "%s :No such channel", + [IRC_ERR_CANNOTSENDTOCHAN] = "%s :Cannot send to channel", [IRC_ERR_NOORIGIN] = ":No origin specified", [IRC_ERR_NORECIPIENT] = ":No recipient given (%s)", [IRC_ERR_NOTEXTTOSEND] = ":No text to send", @@ -685,9 +760,20 @@ static const char *g_default_replies[] = [IRC_ERR_NONICKNAMEGIVEN] = ":No nickname given", [IRC_ERR_ERRONEOUSNICKNAME] = "%s :Erroneous nickname", [IRC_ERR_NICKNAMEINUSE] = "%s :Nickname is already in use", + [IRC_ERR_NOTONCHANNEL] = "%s :You're not on that channel", + [IRC_ERR_SUMMONDISABLED] = ":SUMMON has been disabled", + [IRC_ERR_USERSDISABLED] = ":USERS has been disabled", [IRC_ERR_NOTREGISTERED] = ":You have not registered", [IRC_ERR_NEEDMOREPARAMS] = "%s :Not enough parameters", [IRC_ERR_ALREADYREGISTERED] = ":Unauthorized command (already registered)", + [IRC_ERR_CHANNELISFULL] = "%s :Cannot join channel (+l)", + [IRC_ERR_INVITEONLYCHAN] = "%s :Cannot join channel (+i)", + [IRC_ERR_BANNEDFROMCHAN] = "%s :Cannot join channel (+b)", + [IRC_ERR_BADCHANNELKEY] = "%s :Cannot join channel (+k)", + [IRC_ERR_BADCHANMASK] = "%s :Bad Channel Mask", + [IRC_ERR_CHANOPRIVSNEEDED] = "%s :You're not channel operator", + + [IRC_ERR_USERSDONTMATCH] = ":Cannot change mode for other users", }; // XXX: this way we cannot typecheck the arguments, so we must be careful @@ -709,15 +795,18 @@ irc_send_reply (struct client *c, int id, ...) str_free (&tmp); } +#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) - { - irc_send_reply (c, IRC_ERR_NOMOTD); - return; - } + 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++) @@ -725,13 +814,130 @@ irc_send_motd (struct client *c) irc_send_reply (c, IRC_RPL_ENDOFMOTD); } -static bool -client_on_channel (struct client *c, const struct channel *chan) +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 (!irc_strcmp (iter->nickname, c->nickname)) - return true; - return false; + return iter; + return NULL; +} + +static struct channel_user * +channel_add_user (struct channel *chan, const struct client *c) +{ + size_t nick_len = strlen (c->nickname); + struct channel_user *link = xcalloc (1, sizeof *link + nick_len + 1); + memcpy (link->nickname, c->nickname, nick_len + 1); + 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; +} + +static void +channel_destroy_if_empty (struct server_context *ctx, struct channel *chan) +{ + if (!chan->users) + { + str_map_set (&ctx->channels, chan->name, NULL); + channel_free (chan); + free (chan); + } +} + +static bool +client_in_mask_list (const struct client *c, const struct str_vector *mask) +{ + struct str client; + str_init (&client); + str_append_printf (&client, "%s!%s@%s", + c->nickname, c->username, c->hostname); + irc_strxfrm (client.str, client.str, client.len); + bool result = false; + for (size_t i = 0; i < mask->len; i++) + // FIXME: irc_strxfrm() for the mask (save in canonical format?) + if (!fnmatch (client.str, mask->vector[i], 0)) + { + result = true; + break; + } + str_free (&client); + return result; +} + +static void +client_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; + + // When we're unregistering, the str_map_find() will return zero, + // which will prevent sending the QUIT message to ourselves. + for (struct channel_user *iter = chan->users; iter; iter = iter->next) + str_map_set (&targets, iter->nickname, + str_map_find (&c->ctx->users, iter->nickname)); + } + + str_map_iter_init (&iter, &targets); + struct client *target; + while ((target = str_map_iter_next (&iter))) + irc_send (target, "%s", message); +} + +static void +client_unregister (struct client *c, const char *reason) +{ + if (!c->registered) + return; + + // Make the user effectively non-existent + str_map_set (&c->ctx->users, c->nickname, NULL); + + char *message = xstrdup_printf (":%s!%s@%s QUIT :%s", + c->nickname, c->username, c->hostname, reason); + client_send_to_roommates (c, message); + free (message); + + struct str_map_iter iter; + str_map_iter_init (&iter, &c->ctx->channels); + struct channel *chan; + while ((chan = str_map_iter_next (&iter))) + { + struct channel_user *user; + if (!(user = channel_get_user (chan, c))) + continue; + channel_remove_user (chan, user); + channel_destroy_if_empty (c->ctx, chan); + } + + free (c->nickname); + c->nickname = NULL; + c->registered = false; } static void @@ -754,7 +960,7 @@ irc_send_lusers (struct client *c) struct channel *chan; while ((chan = str_map_iter_next (&iter))) if (!(chan->modes & IRC_CHAN_MODE_SECRET) - || client_on_channel (c, chan)) + || channel_get_user (chan, c)) n_channels++; irc_send_reply (c, IRC_RPL_LUSERCLIENT, @@ -795,7 +1001,7 @@ irc_try_finish_registration (struct client *c) irc_send_motd (c); char *mode = client_get_mode (c); - if (mode) + if (*mode) irc_send (c, ":%s MODE %s :+%s", c->nickname, c->nickname, mode); free (mode); } @@ -816,54 +1022,45 @@ irc_handle_nick (const struct irc_message *msg, struct client *c) { struct server_context *ctx = c->ctx; - if (c->registered) - { - irc_send_reply (c, IRC_ERR_ALREADYREGISTERED); - return; - } if (msg->params.len < 1) - { - irc_send_reply (c, IRC_ERR_NONICKNAMEGIVEN); - return; - } + 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) { - irc_send_reply (c, IRC_ERR_ERRONEOUSNICKNAME, nickname); - return; + char *message = xstrdup_printf (":%s!%s@%s NICK :%s", + c->nickname, c->username, c->hostname, nickname); + client_send_to_roommates (c, 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); } - if (str_map_find (&ctx->users, nickname)) - { - irc_send_reply (c, IRC_ERR_NICKNAMEINUSE, nickname); - return; - } - - // Allocate the nickname c->nickname = xstrdup (nickname); str_map_set (&ctx->users, nickname, c); - irc_try_finish_registration (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) - { - irc_send_reply (c, IRC_ERR_ALREADYREGISTERED); - return; - } + RETURN_WITH_REPLY (c, IRC_ERR_ALREADYREGISTERED); if (msg->params.len < 4) - { - irc_send_reply (c, IRC_ERR_NEEDMOREPARAMS, msg->command); - return; - } + RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command); const char *username = msg->params.vector[0]; const char *mode = msg->params.vector[1]; @@ -892,10 +1089,7 @@ static void irc_handle_userhost (const struct irc_message *msg, struct client *c) { if (msg->params.len < 1) - { - irc_send_reply (c, IRC_ERR_NEEDMOREPARAMS, msg->command); - return; - } + RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command); struct str reply; str_init (&reply); @@ -964,27 +1158,23 @@ irc_handle_pong (const struct irc_message *msg, struct client *c) static void irc_handle_quit (const struct irc_message *msg, struct client *c) { - struct str reason; - str_init (&reason); - str_append_printf (&reason, "Quit: %s", + char *reason = xstrdup_printf ("Quit: %s", msg->params.len > 0 ? msg->params.vector[0] : c->nickname); - irc_close_link (c, reason.str); - str_free (&reason); + irc_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])) - irc_send_reply (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[0]); - else - { - 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); - } + 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 @@ -998,27 +1188,126 @@ irc_handle_version (const struct irc_message *msg, struct client *c) } static void -irc_handle_privmsg (const struct irc_message *msg, struct client *c) +irc_channel_multicast (struct channel *chan, const char *message) { - if (msg->params.len < 1) - irc_send_reply (c, IRC_ERR_NORECIPIENT, msg->command); - else if (msg->params.len < 2 || !*msg->params.vector[1]) - irc_send_reply (c, IRC_ERR_NOTEXTTOSEND); + for (struct channel_user *iter = chan->users; iter; iter = iter->next) + { + struct client *c = str_map_find (&chan->ctx->users, iter->nickname); + irc_send (c, "%s", message); + } +} + +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); +} + +static bool +irc_maybe_send_channel_list (struct client *c, struct channel *chan, + const char *mode_string) +{ + if (*mode_string == '+') + mode_string++; + + if (!strcmp (mode_string, "b")) + irc_send_channel_list (c, chan->name, &chan->ban_list, + IRC_RPL_BANLIST, IRC_RPL_ENDOFBANLIST); + else if (!strcmp (mode_string, "e")) + irc_send_channel_list (c, chan->name, &chan->exception_list, + IRC_RPL_EXCEPTLIST, IRC_RPL_ENDOFEXCEPTLIST); + else if (!strcmp (mode_string, "I")) + irc_send_channel_list (c, chan->name, &chan->invite_list, + IRC_RPL_INVITELIST, IRC_RPL_ENDOFINVITELIST); else + return false; + return true; +} + +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); + if (client) + { + if (irc_strcmp (target, c->nickname)) + RETURN_WITH_REPLY (c, IRC_ERR_USERSDONTMATCH); + + char *mode = client_get_mode (client); + irc_send_reply (c, IRC_RPL_UMODEIS, mode); + free (mode); + + // TODO: mode modification + return; + } + + struct channel *chan = str_map_find (&c->ctx->channels, target); + if (chan) { - const char *target = msg->params.vector[0]; - const char *text = msg->params.vector[1]; - struct client *target_c = str_map_find (&c->ctx->users, target); - if (!target_c) + if (msg->params.len < 2) { - irc_send_reply (c, IRC_ERR_NOSUCHNICK, target); + char *mode = channel_get_mode (chan, channel_get_user (chan, c)); + irc_send_reply (c, IRC_RPL_CHANNELMODEIS, target, mode); + free (mode); return; } + if (msg->params.len < 3 + && irc_maybe_send_channel_list (c, chan, msg->params.vector[1])) + return; + + // TODO: mode modification + return; + } + + irc_send_reply (c, IRC_ERR_NOSUCHNICK, target); +} + +static void +irc_handle_privmsg (const struct irc_message *msg, struct client *c) +{ + 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) + { + irc_send (client, ":%s!%s@%s PRIVMSG %s :%s", + c->nickname, c->username, c->hostname, target, text); + return; + } - // TODO: channels - irc_send (target_c, ":%s!%s@%s PRIVMSG %s :%s", + 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 PRIVMSG %s :%s", c->nickname, c->username, c->hostname, target, text); + irc_channel_multicast (chan, message); + free (message); + return; } + + irc_send_reply (c, IRC_ERR_NOSUCHNICK, target); } static void @@ -1036,10 +1325,7 @@ 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])) - { - irc_send_reply (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[1]); - return; - } + RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[1]); struct channel *chan; if (msg->params.len == 0) @@ -1048,7 +1334,7 @@ irc_handle_list (const struct irc_message *msg, struct client *c) 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)) - || client_on_channel (c, chan)) + || channel_get_user (chan, c)) irc_send_rpl_list (c, chan); } else @@ -1059,13 +1345,298 @@ irc_handle_list (const struct irc_message *msg, struct client *c) 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) - || client_on_channel (c, chan))) + || 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_send_rpl_namreply (struct client *c, const struct channel *chan) +{ + 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) + { + struct client *target = str_map_find (&c->ctx->users, iter->nickname); + if (!on_channel && (target->mode & IRC_USER_MODE_INVISIBLE)) + continue; + + struct str result; + str_init (&result); + if (iter->modes & IRC_CHAN_MODE_OPERATOR) + str_append_c (&result, '@'); + else if (iter->modes & IRC_CHAN_MODE_VOICE) + str_append_c (&result, '+'); + str_append (&result, target->nickname); + str_vector_add_owned (&nicks, str_steal (&result)); + } + + if (nicks.len) + { + char *reply = join_str_vector (&nicks, ' '); + irc_send_reply (c, IRC_RPL_NAMREPLY, type, chan->name, reply); + free (reply); + } + 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_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); + + // TODO + // If no <channel> parameter is given, a list of all channels and their + // occupants is returned. At the end of this list, a list of users who + // are visible but either not on any channel or not on a visible channel + // are listed as being on `channel' "*". + 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); + irc_send_reply (c, IRC_RPL_ENDOFNAMES, channels.vector[i]); + } + str_vector_free (&channels); + } +} + +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); +} + +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); + + free (chan->topic); + chan->topic = xstrdup (msg->params.vector[1]); + + char *message = xstrdup_printf (":%s!%s@%s TOPIC %s :%s", + c->nickname, c->username, c->hostname, target, chan->topic); + irc_channel_multicast (chan, message); + 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); + else + irc_send (c, "%s", message); + free (message); + + channel_remove_user (chan, user); + 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 = str_map_iter_next (&iter), *next; + while (chan) + { + // 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); + chan = next; + } +} + +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_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); + user_mode = IRC_CHAN_MODE_OPERATOR; + + chan = xcalloc (1, sizeof *chan); + channel_init (chan); + str_map_set (&c->ctx->channels, channel_name, chan); + + chan->ctx = c->ctx; + chan->name = xstrdup (channel_name); + } + else if (channel_get_user (chan, c)) + return; + + if ((chan->modes & IRC_CHAN_MODE_INVITE_ONLY) + && !client_in_mask_list (c, &chan->invite_list)) + // TODO: exceptions caused by INVITE + 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)) + RETURN_WITH_REPLY (c, IRC_ERR_BANNEDFROMCHAN, channel_name); + + 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); + else + irc_send (c, "%s", message); + free (message); + + irc_send_rpl_topic (c, chan); + irc_send_rpl_namreply (c, chan); +} + +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); + } +} + // ----------------------------------------------------------------------------- struct irc_command @@ -1079,6 +1650,7 @@ 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: more commands, see RFC 2812 :! static const struct irc_command message_handlers[] = { @@ -1094,9 +1666,17 @@ irc_register_handlers (struct server_context *ctx) { "QUIT", false, irc_handle_quit }, { "TIME", true, irc_handle_time }, { "VERSION", true, irc_handle_version }, + { "USERS", true, irc_handle_users }, + { "SUMMON", true, irc_handle_summon }, + { "AWAY", true, irc_handle_away }, + { "MODE", true, irc_handle_mode }, { "PRIVMSG", true, irc_handle_privmsg }, + { "JOIN", true, irc_handle_join }, + { "PART", true, irc_handle_part }, + { "TOPIC", true, irc_handle_topic }, { "LIST", true, irc_handle_list }, + { "NAMES", true, irc_handle_names }, }; for (size_t i = 0; i < N_ELEMENTS (message_handlers); i++) |