From b58ee27362ac285e3839ed051f4319118280ad5a Mon Sep 17 00:00:00 2001 From: Přemysl Janouch Date: Mon, 14 Jul 2014 02:35:38 +0200 Subject: Implement client registration And shuffle around some functions so that they form logical blocks (at least I've tried; it's not that easy when you try to avoid forward declarations). --- src/kike.c | 461 ++++++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 381 insertions(+), 80 deletions(-) diff --git a/src/kike.c b/src/kike.c index a927350..cbe47ec 100644 --- a/src/kike.c +++ b/src/kike.c @@ -106,8 +106,9 @@ enum validation_result }; // Everything as per RFC 2812 -#define IRC_NICKNAME_MAX 9 -#define IRC_HOSTNAME_MAX 63 +#define IRC_MAX_NICKNAME 9 +#define IRC_MAX_HOSTNAME 63 +#define IRC_MAX_MESSAGE_LENGTH 510 static bool irc_regex_match (const char *regex, const char *s) @@ -156,7 +157,7 @@ irc_validate_hostname (const char *hostname) return VALIDATION_ERROR_EMPTY; if (!irc_regex_match ("^" SN "(\\." SN ")*$", hostname)) return VALIDATION_ERROR_INVALID; - if (strlen (hostname) > IRC_HOSTNAME_MAX) + if (strlen (hostname) > IRC_MAX_HOSTNAME) return VALIDATION_ERROR_TOO_LONG; return VALIDATION_OK; } @@ -180,6 +181,12 @@ irc_is_valid_host (const char *host) || 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) { @@ -187,7 +194,7 @@ irc_validate_nickname (const char *nickname) return VALIDATION_ERROR_EMPTY; if (!irc_regex_match ("^[" LE SP "][-0-9" LE SP "]*$", nickname)) return VALIDATION_ERROR_INVALID; - if (strlen (nickname) > IRC_NICKNAME_MAX) + if (strlen (nickname) > IRC_MAX_NICKNAME) return VALIDATION_ERROR_TOO_LONG; return VALIDATION_OK; } @@ -212,6 +219,7 @@ enum IRC_USER_MODE_RX_SERVER_NOTICES = (1 << 4) }; +// TODO: rename to client? struct connection { struct connection *next; ///< The next link in a chain @@ -224,9 +232,10 @@ struct connection struct str write_buffer; ///< Output yet to be sent out unsigned initialized : 1; ///< Has any data been received yet? + unsigned registered : 1; ///< The user has registered + unsigned ssl_rx_want_tx : 1; ///< SSL_read() wants to write unsigned ssl_tx_want_rx : 1; ///< SSL_write() wants to read - SSL *ssl; ///< SSL connection char *nickname; ///< IRC nickname (main identifier) @@ -348,6 +357,7 @@ struct server_context char *server_name; ///< Our server name struct str_map users; ///< Maps nicknames to connections struct str_map channels; ///< Maps channel names to data + struct str_map handlers; ///< Message handlers struct poller poller; ///< Manages polled description bool quitting; ///< User requested quitting @@ -369,8 +379,12 @@ server_context_init (struct server_context *self) self->server_name = NULL; str_map_init (&self->users); + self->users.key_cmp = irc_strcmp; // TODO: set channel_free() as the free function? str_map_init (&self->channels); + self->channels.key_cmp = irc_strcmp; + str_map_init (&self->handlers); + self->handlers.key_cmp = irc_strcmp; poller_init (&self->poller); self->quitting = false; @@ -402,6 +416,7 @@ server_context_free (struct server_context *self) free (self->server_name); str_map_free (&self->users); str_map_free (&self->channels); + str_map_free (&self->handlers); poller_free (&self->poller); str_vector_free (&self->motd); @@ -420,111 +435,317 @@ enum NETWORK_ERROR_FAILED }; -static bool -irc_autodetect_ssl (struct connection *conn) +static void +connection_kill (struct connection *conn, const char *reason) { - // 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. + // TODO: multicast a QUIT message with `reason' || "Client exited" + (void) reason; - char buf[3]; -start: - switch (recv (conn->socket_fd, buf, sizeof buf, MSG_PEEK)) + // TODO: do further cleanup if the client has successfully registered etc. + + struct server_context *ctx = conn->ctx; + ssize_t i = poller_find_by_fd (&ctx->poller, conn->socket_fd); + if (i != -1) + poller_remove_at_index (&ctx->poller, i); + + xclose (conn->socket_fd); + conn->socket_fd = -1; + connection_free (conn); + LIST_UNLINK (ctx->clients, conn); + free (conn); +} + +static void +irc_send_str (struct connection *conn, const struct str *s) +{ + // TODO: kill the connection above some "SendQ" threshold (careful!) + + str_append_data (&conn->write_buffer, s->str, + s->len > IRC_MAX_MESSAGE_LENGTH ? IRC_MAX_MESSAGE_LENGTH : s->len); + str_append (&conn->write_buffer, "\r\n"); +} + +static void irc_send (struct connection *conn, + const char *format, ...) ATTRIBUTE_PRINTF (2, 3); + +static void +irc_send (struct connection *conn, 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); + + irc_send_str (conn, &tmp); + str_free (&tmp); +} + +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); +} + +// --- IRC command handling ---------------------------------------------------- + +enum +{ + IRC_RPL_WELCOME = 1, + IRC_RPL_YOURHOST = 2, + IRC_RPL_CREATED = 3, + IRC_RPL_MYINFO = 4, + + IRC_RPL_MOTD = 372, + IRC_RPL_MOTDSTART = 375, + IRC_RPL_ENDOFMOTD = 376, + + IRC_ERR_NOORIGIN = 409, + IRC_ERR_UNKNOWNCOMMAND = 421, + IRC_ERR_NOMOTD = 422, + IRC_ERR_NONICKNAMEGIVEN = 431, + IRC_ERR_ERRONEOUSNICKNAME = 432, + IRC_ERR_NICKNAMEINUSE = 433, + IRC_ERR_NOTREGISTERED = 451, + IRC_ERR_NEEDMOREPARAMS = 461, + IRC_ERR_ALREADYREGISTERED = 462 +}; + +static const char *g_default_replies[] = +{ + [IRC_RPL_WELCOME] = ":Welcome to the Internet Relay Network %s!%s@%s", + [IRC_RPL_YOURHOST] = ":Your host is %s, running version %s", + [IRC_RPL_CREATED] = ":This server was created %s", + [IRC_RPL_MYINFO] = "%s %s %s %s", + + [IRC_RPL_MOTD] = ":- %s", + [IRC_RPL_MOTDSTART] = ":- %s Message of the day - ", + [IRC_RPL_ENDOFMOTD] = ":End of MOTD command", + + [IRC_ERR_NOORIGIN] = ":No origin specified", + [IRC_ERR_UNKNOWNCOMMAND] = "%s: Unknown command", + [IRC_ERR_NOMOTD] = ":MOTD File is missing", + [IRC_ERR_NONICKNAMEGIVEN] = ":No nickname given", + [IRC_ERR_ERRONEOUSNICKNAME] = "%s :Erroneous nickname", + [IRC_ERR_NICKNAMEINUSE] = "%s :Nickname is already in use", + [IRC_ERR_NOTREGISTERED] = "%s :You have not registered", + [IRC_ERR_NEEDMOREPARAMS] = "%s :Not enough parameters", + [IRC_ERR_ALREADYREGISTERED] = ":Unauthorized command (already registered)", +}; + +// XXX: this way we cannot typecheck the arguments, so we must be careful +static void +irc_send_reply (struct connection *conn, int id, ...) +{ + struct str tmp; + str_init (&tmp); + + va_list ap; + va_start (ap, id); + str_append_printf (&tmp, ":%s %03d %s ", + conn->ctx->server_name, id, conn->nickname ? conn->nickname : ""); + str_append_vprintf (&tmp, + irc_get_text (conn->ctx, id, g_default_replies[id]), ap); + va_end (ap); + + irc_send_str (conn, &tmp); + str_free (&tmp); +} + +static void +irc_send_motd (struct connection *conn) +{ + struct server_context *ctx = conn->ctx; + if (!ctx->motd.len) { - 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; + irc_send_reply (conn, IRC_ERR_NOMOTD); + return; } - return false; + + irc_send_reply (conn, IRC_RPL_MOTDSTART, ctx->server_name); + for (size_t i = 0; i < ctx->motd.len; i++) + irc_send_reply (conn, IRC_RPL_MOTD, ctx->motd.vector[i]); + irc_send_reply (conn, IRC_RPL_ENDOFMOTD); } -static int -irc_ssl_verify_callback (int verify_ok, X509_STORE_CTX *ctx) +static void +irc_try_finish_registration (struct connection *conn) { - (void) verify_ok; - (void) ctx; + if (!conn->nickname || !conn->username || !conn->realname) + return; + + conn->registered = true; + irc_send_reply (conn, IRC_RPL_WELCOME, + conn->nickname, conn->username, conn->hostname); + + irc_send_reply (conn, IRC_RPL_YOURHOST, + conn->ctx->server_name, PROGRAM_VERSION); + // The purpose of this message eludes me + irc_send_reply (conn, IRC_RPL_CREATED, __DATE__); + irc_send_reply (conn, IRC_RPL_MYINFO, + conn->ctx->server_name, PROGRAM_VERSION, + IRC_SUPPORTED_USER_MODES, IRC_SUPPORTED_CHAN_MODES); + + // Although not strictly required, bots often need this to work + irc_send_motd (conn); +} - // 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 void +irc_handle_pass (const struct irc_message *msg, struct connection *conn) +{ + if (conn->registered) + irc_send_reply (conn, IRC_ERR_ALREADYREGISTERED); + else if (msg->params.len < 1) + irc_send_reply (conn, IRC_ERR_NEEDMOREPARAMS, msg->command); + + // We have SSL client certificates for this purpose; ignoring } -static bool -connection_initialize_ssl (struct connection *conn) +static void +irc_handle_nick (const struct irc_message *msg, struct connection *conn) { - // SSL support not enabled - if (!conn->ctx->ssl_ctx) - return false; + struct server_context *ctx = conn->ctx; - conn->ssl = SSL_new (conn->ctx->ssl_ctx); - if (!conn->ssl) - goto error_ssl_1; + if (conn->registered) + { + irc_send_reply (conn, IRC_ERR_ALREADYREGISTERED); + return; + } + if (msg->params.len < 1) + { + irc_send_reply (conn, IRC_ERR_NONICKNAMEGIVEN); + return; + } - if (!SSL_set_fd (conn->ssl, conn->socket_fd)) - goto error_ssl_2; - SSL_set_accept_state (conn->ssl); - return true; + const char *nickname = msg->params.vector[0]; + if (irc_validate_nickname (nickname) != VALIDATION_OK) + { + irc_send_reply (conn, IRC_ERR_ERRONEOUSNICKNAME, nickname); + return; + } + if (str_map_find (&ctx->users, nickname)) + { + irc_send_reply (conn, IRC_ERR_NICKNAMEINUSE, nickname); + return; + } + if (conn->nickname) + { + str_map_set (&ctx->users, conn->nickname, NULL); + free (conn->nickname); + } -error_ssl_2: - SSL_free (conn->ssl); - conn->ssl = NULL; -error_ssl_1: - // XXX: these error strings are really nasty; also there could be - // multiple errors on the OpenSSL stack. - print_debug ("%s: %s: %s", "could not initialize SSL", - conn->hostname, ERR_error_string (ERR_get_error (), NULL)); - return false; + // Allocate the nickname + conn->nickname = xstrdup (nickname); + str_map_set (&ctx->users, nickname, conn); + + irc_try_finish_registration (conn); } static void -connection_kill (struct connection *conn, const char *reason) +irc_handle_user (const struct irc_message *msg, struct connection *conn) { - // TODO: multicast a QUIT message with `reason' || "Client exited" - (void) reason; + if (conn->registered) + { + irc_send_reply (conn, IRC_ERR_ALREADYREGISTERED); + return; + } + if (msg->params.len < 4) + { + irc_send_reply (conn, IRC_ERR_NEEDMOREPARAMS, msg->command); + return; + } - // TODO: do further cleanup if the client has successfully registered etc. + const char *username = msg->params.vector[0]; + const char *mode = msg->params.vector[1]; + const char *realname = msg->params.vector[3]; - struct server_context *ctx = conn->ctx; - ssize_t i = poller_find_by_fd (&ctx->poller, conn->socket_fd); - if (i != -1) - poller_remove_at_index (&ctx->poller, i); + // Unfortunately the protocol doesn't give us any means of rejecting it + if (!irc_is_valid_user (username)) + username = "xxx"; - xclose (conn->socket_fd); - conn->socket_fd = -1; - connection_free (conn); - LIST_UNLINK (ctx->clients, conn); - free (conn); + free (conn->username); + conn->username = xstrdup (username); + free (conn->realname); + conn->realname = xstrdup (realname); + + unsigned long m; + if (xstrtoul (&m, mode, 10)) + { + if (m & 4) conn->mode |= IRC_USER_MODE_RX_WALLOPS; + if (m & 8) conn->mode |= IRC_USER_MODE_INVISIBLE; + } + + irc_try_finish_registration (conn); +} + +static void +irc_handle_ping (const struct irc_message *msg, struct connection *conn) +{ + // XXX: the RFC is pretty incomprehensible about the exact usage + if (msg->params.len < 1) + irc_send_reply (conn, IRC_ERR_NOORIGIN); + else + irc_send (conn, ":%s PONG :%s", + conn->ctx->server_name, msg->params.vector[0]); +} + +// ----------------------------------------------------------------------------- + +struct irc_command +{ + const char *name; + bool requires_registration; + void (*handler) (const struct irc_message *, struct connection *); +}; + +static void +irc_register_handlers (struct server_context *ctx) +{ + static const struct irc_command message_handlers[] = + { + { "PASS", false, irc_handle_pass }, + { "NICK", false, irc_handle_nick }, + { "USER", false, irc_handle_user }, + + { "PING", true, irc_handle_ping } + }; + + 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->handler); + } } static void irc_process_message (const struct irc_message *msg, const char *raw, void *user_data) { + (void) raw; + + // XXX: we may want to discard everything following a QUIT etc. + // We can set a flag within the connection object. + // TODO: see RFC 2812 :! + struct connection *conn = user_data; - // TODO + struct irc_command *cmd = str_map_find (&conn->ctx->handlers, msg->command); + if (!cmd) + irc_send_reply (conn, IRC_ERR_UNKNOWNCOMMAND, + "%s: Unknown command", msg->command); + else if (cmd->requires_registration && !conn->registered) + irc_send_reply (conn, IRC_ERR_NOTREGISTERED); + else + cmd->handler (msg, conn); } +// --- Network I/O ------------------------------------------------------------- + static bool irc_try_read (struct connection *conn) { @@ -665,6 +886,72 @@ irc_try_write_ssl (struct connection *conn) return true; } +static bool +irc_autodetect_ssl (struct connection *conn) +{ + // 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 (conn->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 +connection_initialize_ssl (struct connection *conn) +{ + // SSL support not enabled + if (!conn->ctx->ssl_ctx) + return false; + + conn->ssl = SSL_new (conn->ctx->ssl_ctx); + if (!conn->ssl) + goto error_ssl_1; + + if (!SSL_set_fd (conn->ssl, conn->socket_fd)) + goto error_ssl_2; + SSL_set_accept_state (conn->ssl); + return true; + +error_ssl_2: + SSL_free (conn->ssl); + conn->ssl = NULL; +error_ssl_1: + // XXX: these error strings are really nasty; also there could be + // multiple errors on the OpenSSL stack. + print_debug ("%s: %s: %s", "could not initialize SSL", + conn->hostname, ERR_error_string (ERR_get_error (), NULL)); + return false; +} + static void on_irc_client_ready (const struct pollfd *pfd, void *user_data) { @@ -764,6 +1051,19 @@ on_irc_connection_available (const struct pollfd *pfd, void *user_data) } } +// ----------------------------------------------------------------------------- + +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 (struct server_context *ctx) { @@ -1088,6 +1388,7 @@ main (int argc, char *argv[]) struct server_context ctx; server_context_init (&ctx); + irc_register_handlers (&ctx); struct error *e = NULL; if (!read_config_file (&ctx.config, &e)) -- cgit v1.2.3-70-g09d2