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