summaryrefslogtreecommitdiff
path: root/irc.c
diff options
context:
space:
mode:
Diffstat (limited to 'irc.c')
-rw-r--r--irc.c931
1 files changed, 931 insertions, 0 deletions
diff --git a/irc.c b/irc.c
new file mode 100644
index 0000000..565a00d
--- /dev/null
+++ b/irc.c
@@ -0,0 +1,931 @@
+#include "irc.h"
+#include <lac.h>
+#include <string.h>
+
+typedef struct IrcMessage IrcMessage;
+typedef struct CommandInfo CommandInfo;
+
+typedef enum
+{
+ /* Errors */
+ ERR_NOSUCHNICK = 401,
+ ERR_NOSUCHSERVER = 402,
+ ERR_NOSUCHCHANNEL = 403,
+ ERR_CANNOTSENDTOCHAN = 404,
+ ERR_TOOMANYCHANNELS = 405,
+ ERR_WASNOSUCHNICK = 406,
+ ERR_TOOMANYTARGETS = 407,
+ ERR_NOORIGIN = 409,
+ ERR_NORECIPIENT = 411,
+ ERR_NOTEXTTOSEND = 412,
+ ERR_NOTOPLEVEL = 413,
+ ERR_WILDTOPLEVEL = 414,
+ ERR_UNKNOWNCOMMAND = 421,
+ ERR_NOMOTD = 422,
+ ERR_NOADMININFO = 423,
+ ERR_FILEERROR = 424,
+ ERR_NONICKNAMEGIVEN = 431,
+ ERR_ERRONEUSNICKNAME = 432,
+ ERR_NICKNAMEINUSE = 433,
+ ERR_NICKCOLLISION = 436,
+ ERR_USERNOTINCHANNEL = 441,
+ ERR_NOTONCHANNEL = 442,
+ ERR_USERONCHANNEL = 443,
+ ERR_NOLOGIN = 444,
+ ERR_SUMMONDISABLED = 445,
+ ERR_USERSDISABLED = 446,
+ ERR_NOTREGISTERED = 451,
+ ERR_NEEDMOREPARAMS = 461,
+ ERR_ALREADYREGISTRED = 462,
+ ERR_NOPERMFORHOST = 463,
+ ERR_PASSWDMISMATCH = 464,
+ ERR_YOUREBANNEDCREEP = 465,
+ ERR_KEYSET = 467,
+ ERR_CHANNELISFULL = 471,
+ ERR_UNKNOWNMODE = 472,
+ ERR_INVITEONLYCHAN = 473,
+ ERR_BANNEDFROMCHAN = 474,
+ ERR_BADCHANNELKEY = 475,
+ ERR_NOPRIVILEGES = 481,
+ ERR_CHANOPRIVSNEEDED = 482,
+ ERR_CANTKILLSERVER = 483,
+ ERR_NOOPERHOST = 491,
+ ERR_UMODEUNKNOWNFLAG = 501,
+ ERR_USERSDONTMATCH = 502,
+
+ /* Replies */
+ RPL_NONE = 300,
+ RPL_USERHOST = 302,
+ RPL_ISON = 303,
+ RPL_AWAY = 301,
+ RPL_UNAWAY = 305,
+ RPL_NOWAWAY = 306,
+ RPL_WHOISUSER = 311,
+ RPL_WHOISSERVER = 312,
+ RPL_WHOISOPERATOR = 313,
+ RPL_WHOISIDLE = 317,
+ RPL_ENDOFWHOIS = 318,
+ RPL_WHOWASUSER = 314,
+ RPL_ENDOFWHOWAS = 369,
+ RPL_LISTSTART = 321,
+ RPL_LIST = 322,
+ RPL_LISTEND = 323,
+ RPL_CHANNELMODEIS = 324,
+ RPL_NOTOPIC = 331,
+ RPL_TOPIC = 332,
+ RPL_INVITING = 341,
+ RPL_SUMMONING = 342,
+ RPL_VERSION = 351,
+ RPL_WHOREPLY = 352,
+ RPL_ENDOFWHO = 315,
+ RPL_NAMREPLY = 353,
+ RPL_ENDOFNAMES = 366,
+ RPL_LINKS = 364,
+ RPL_ENDOFLINKS = 365,
+ RPL_BANLIST = 367,
+ RPL_ENDOFBANLIST = 368,
+ RPL_INFO = 371,
+ RPL_ENDOFINFO = 374,
+ RPL_MOTDSTART = 375,
+ RPL_MOTD = 372,
+ RPL_ENDOFMOTD = 376,
+ RPL_YOUREOPER = 381,
+ RPL_REHASHING = 382,
+ RPL_TIME = 391,
+ RPL_USERSSTART = 392,
+ RPL_USERS = 393,
+ RPL_ENDOFUSERS = 394,
+ RPL_NOUSERS = 395,
+ RPL_TRACELINK = 200,
+ RPL_TRACECONNECTING = 201,
+ RPL_TRACEHANDSHAKE = 202,
+ RPL_TRACEUNKNOWN = 203,
+ RPL_TRACEOPERATOR = 204,
+ RPL_TRACEUSER = 205,
+ RPL_TRACESERVER = 206,
+ RPL_TRACENEWTYPE = 208,
+ RPL_TRACELOG = 261,
+ RPL_STATSLINKINFO = 211,
+ RPL_STATSCOMMANDS = 212,
+ RPL_STATSCLINE = 213,
+ RPL_STATSNLINE = 214,
+ RPL_STATSILINE = 215,
+ RPL_STATSKLINE = 216,
+ RPL_STATSYLINE = 218,
+ RPL_ENDOFSTATS = 219,
+ RPL_STATSLLINE = 241,
+ RPL_STATSUPTIME = 242,
+ RPL_STATSOLINE = 243,
+ RPL_STATSHLINE = 244,
+ RPL_UMODEIS = 221,
+ RPL_LUSERCLIENT = 251,
+ RPL_LUSEROP = 252,
+ RPL_LUSERUNKNOWN = 253,
+ RPL_LUSERCHANNELS = 254,
+ RPL_LUSERME = 255,
+ RPL_ADMINME = 256,
+ RPL_ADMINLOC1 = 257,
+ RPL_ADMINLOC2 = 258,
+ RPL_ADMINEMAIL = 259,
+
+ /* These codes are not specified by the RFC spec, just
+ * used internally. (IRC can't use them, because they
+ * have more than three digits)
+ */
+ CMD_NOTICE = 9001,
+ CMD_PRIVMSG = 9002,
+ CMD_PASS = 9003,
+ CMD_NICK = 9004,
+ CMD_USER = 9005,
+
+ CMD_UNKNOWN = 10000, /* message not understood */
+} ReplyCode;
+
+#define CR 0x0d
+#define LF 0x0a
+#define NUL 0x00
+#define BELL 0x07
+#define SPACE 0x20
+#define COMMA 0x2c
+
+static const gchar CRLF[] = { CR, LF, '\0' };
+
+struct CommandInfo
+{
+ const ReplyCode code;
+ const char *text;
+};
+
+static const CommandInfo commands[] =
+{
+ { CMD_NOTICE, "NOTICE" },
+ { CMD_PRIVMSG, "PRIVMSG" },
+ { CMD_USER, "USER" },
+ { CMD_PASS, "PASS" },
+ { CMD_NICK, "NICK" },
+ { CMD_UNKNOWN, "Unknown command" }
+};
+
+static ReplyCode
+command_to_code (const char *start, const char *end)
+{
+ char *command = g_strndup (start, end - start);
+ ReplyCode code = CMD_UNKNOWN;
+ int i;
+
+ for (i = 0; i < G_N_ELEMENTS (commands); ++i)
+ {
+ if (strcmp (command, commands[i].text) == 0)
+ {
+ code = commands[i].code;
+ break;
+ }
+ }
+
+ return code;
+}
+
+static const char *
+code_to_command (ReplyCode code)
+{
+ const char *cmd = NULL;
+ int i;
+
+ for (i = 0; i < G_N_ELEMENTS (commands); ++i)
+ {
+ if (commands[i].code == code)
+ {
+ cmd = commands[i].text;
+ break;
+ }
+ }
+
+ return cmd;
+}
+
+struct IrcMessage
+{
+ char * prefix;
+ ReplyCode code;
+ GQueue * parameters;
+
+ GList * free_list;
+};
+
+static IrcMessage *message_new_parsed (const char **input,
+ int *length);
+static void message_free (IrcMessage *msg);
+
+static gpointer trace (gpointer data,
+ gpointer free_func,
+ GList **freelist);
+
+/*
+ * Message parser
+ */
+static int
+eat_spaces (const char **input, int *length)
+{
+ int n_spaces;
+
+ n_spaces = 0;
+ while (**input == SPACE)
+ {
+ *input += 1;
+ *length -= 1;
+
+ n_spaces++;
+ }
+
+ return n_spaces;
+}
+
+static const char *
+find_char (const char *input, int length, char c)
+{
+ int i;
+ for (i = 0; i < length; ++i)
+ {
+ if (input[i] == c)
+ return input + i;
+ }
+ return NULL;
+}
+
+static char *
+parse_prefix (const char **input,
+ int *length,
+ GList **free_list,
+ gboolean *error)
+{
+ const char *space;
+
+ space = find_char (*input, *length, SPACE);
+ if (!space || space == *input)
+ {
+ *error = TRUE;
+ return NULL;
+ }
+ else
+ {
+ int prefix_len = space - *input;
+ char *result = g_strndup (*input, prefix_len);
+
+ *input += prefix_len;
+ *length -= prefix_len;
+
+ return trace (result, g_free, free_list);
+ }
+}
+
+static ReplyCode
+parse_command (const char **input,
+ int *length,
+ GList **free_list,
+ gboolean *err)
+{
+ ReplyCode result;
+
+ if (*length < 1)
+ goto error;
+
+ if (g_ascii_isalpha (**input))
+ {
+ const char *start = *input;
+
+ while (*length > 0 && g_ascii_isalpha (**input))
+ {
+ *input += 1;
+ *length -= 1;
+ }
+
+ return command_to_code (start, *input);
+ }
+ else if (g_ascii_isdigit (**input));
+ {
+ int d1, d2, d3;
+
+ if (*length < 3)
+ goto error;
+
+ d1 = (*input)[0] - '0';
+ d2 = (*input)[1] - '0';
+ d3 = (*input)[2] - '0';
+
+ if (d1 < 0 || d1 > 9 ||
+ d2 < 0 || d2 > 9 ||
+ d3 < 0 || d3 > 9)
+ {
+ goto error;
+ }
+
+ *input += 3;
+ *length -= 3;
+
+ return (ReplyCode) d1 * 100 + d2 * 10 + d3;
+ }
+
+ return result;
+
+error:
+ *err = TRUE;
+ return -1;
+}
+
+static gboolean
+is_trailing_char (char c)
+{
+ return c != NUL && c != CR && c != LF;
+}
+
+static gboolean
+is_middle_char (char c)
+{
+ return c != SPACE && c != NUL && c != CR && c != LF;
+}
+
+static char *
+parse_trailing (const char **input,
+ int *length,
+ GList **free_list,
+ gboolean *err)
+{
+ const char *start = *input;
+
+ while (*length > 0 && is_trailing_char (**input))
+ {
+ *input += 1;
+ *length -= 1;
+ }
+
+ if (*input != start)
+ return trace (g_strndup (start, *input - start), g_free, free_list);
+ else
+ return NULL;
+}
+
+static char *
+parse_middle (const char **input,
+ int *length,
+ GList **free_list,
+ gboolean *err)
+{
+ const char *start = *input;
+
+ while (*length > 0 && is_middle_char (**input))
+ {
+ *input += 1;
+ *length -= 1;
+ }
+
+ if (*input == start)
+ goto error;
+
+ return trace (g_strndup (start, *input - start), g_free, free_list);
+
+error:
+ *err = TRUE;
+ return NUL;
+}
+
+static GQueue *
+parse_params (const char **input,
+ int *length,
+ GList **free_list,
+ gboolean *err)
+{
+ GQueue *params = NULL;
+
+ if (*length < 1)
+ goto error;
+
+ if (**input == SPACE)
+ eat_spaces (input, length);
+
+ if (*length > 0)
+ {
+ char *trailing;
+
+ if (**input == ':')
+ {
+ *input += 1;
+ *length -= 1;
+
+ /* <trailing> */
+ trailing = parse_trailing (input, length, free_list, err);
+ if (*err)
+ goto error;
+
+ if (trailing)
+ {
+ params = trace (g_queue_new(), g_queue_free, free_list);
+
+ g_queue_push_tail (params, trailing);
+ }
+ }
+ else if (is_middle_char (**input))
+ {
+ char *middle;
+
+ /* <middle> */
+ middle = parse_middle (input, length, free_list, err);
+ if (*err)
+ goto error;
+
+ /* <params> */
+
+ /* The RFC states that parameters always start with SPACE,
+ * but that is a lie: The final params following a <middle>
+ * can be the empty string
+ */
+ if (**input == SPACE)
+ {
+ params = parse_params (input, length, free_list, err);
+ if (*err)
+ goto error;
+ }
+ else
+ {
+ params = trace (g_queue_new(), g_queue_free, free_list);
+
+ g_queue_push_head (params, middle);
+ }
+ }
+ }
+
+ if (!params)
+ params = trace (g_queue_new(), g_queue_free, free_list);
+
+ return params;
+
+error:
+ *err = TRUE;
+ return NULL;
+}
+
+/* Returns NULL if *input is not a valid message. This will also
+ * happen if we just haven't received enough data yet.
+ */
+static IrcMessage *
+message_new_parsed (const char **input,
+ int *length)
+{
+ IrcMessage *msg = g_new (IrcMessage, 1);
+ const char *crlf;
+ gboolean err = FALSE;
+
+ msg->free_list = NULL;
+
+ if (*length < 2)
+ goto error;
+
+ crlf = strstr (*input, CRLF);
+
+ if (!crlf)
+ goto error;
+
+ if (crlf - *input > 510)
+ goto error;
+
+ msg->prefix = NULL;
+ if (**input == ':')
+ {
+ *input += 1;
+ *length -= 1;
+
+ msg->prefix = parse_prefix (input, length, &msg->free_list, &err);
+
+ if (err)
+ goto error;
+
+ if (eat_spaces (input, length) < 1)
+ goto error;
+ }
+
+ msg->code = parse_command (input, length, &msg->free_list, &err);
+ if (err)
+ goto error;
+
+ msg->parameters = parse_params (input, length, &msg->free_list, &err);
+
+ if (err)
+ goto error;
+
+ if (*length < 2)
+ goto error;
+
+ if (strncmp (*input, CRLF, 2) != 0)
+ goto error;
+
+ *input += 2;
+ *length -= 2;
+
+ return msg;
+
+error:
+ message_free (msg);
+ return NULL;
+}
+
+static void
+message_free (IrcMessage *msg)
+{
+ GList *list;
+
+ for (list = msg->free_list; list != NULL; list = list->next->next)
+ {
+ GDestroyNotify free_func = list->data;
+ gpointer alloc = list->next->data;
+
+ (* free_func) (alloc);
+ }
+
+ g_list_free (msg->free_list);
+
+ g_free (msg);
+}
+
+typedef enum
+{
+ NOT_CONNECTED,
+ CONNECTING,
+ CONNECTED
+} IrcServerState;
+
+struct IrcServer
+{
+ /* information about server */
+ char * hostname;
+ gint port;
+
+ /* information about user */
+ char * local_user_name;
+ char * local_host_name;
+ char * local_domain_name;
+ char * real_name;
+ char * nickname;
+ char * password;
+
+ /* state */
+ IrcServerState state;
+ LacConnection * connection;
+ IrcServerFunc func;
+ gpointer data;
+ GString * unparsed;
+ GQueue * messages;
+};
+
+IrcServer *
+irc_server_new (const char *hostname,
+ gint port,
+ IrcServerFunc func,
+ gpointer data)
+{
+ IrcServer *server;
+
+ g_return_val_if_fail (hostname != NULL, NULL);
+ g_return_val_if_fail (port > 0, NULL);
+ g_return_val_if_fail (func != NULL, NULL);
+
+ server = g_new (IrcServer, 1);
+
+ server->hostname = g_strdup (hostname);
+ server->port = port;
+
+ server->local_user_name = NULL;
+ server->local_host_name = NULL;
+ server->local_domain_name = NULL;
+ server->real_name = NULL;
+ server->nickname = NULL;
+ server->password = NULL;
+
+ server->state = NOT_CONNECTED;
+ server->connection = NULL;
+ server->func = func;
+ server->data = data;
+ server->unparsed = g_string_new ("");
+ server->messages = g_queue_new ();
+
+ return server;
+}
+
+gpointer
+irc_server_get_data (IrcServer *server)
+{
+ g_return_val_if_fail (server != NULL, NULL);
+
+ return server->data;
+}
+
+static void
+server_emit_dns_complete (IrcServer *server)
+{
+}
+
+static void
+server_emit_error (IrcServer *server,
+ const GError *err)
+{
+
+}
+
+static void
+server_emit_connect (IrcServer *server)
+{
+
+}
+
+static void
+server_emit_close (IrcServer *server)
+{
+
+}
+
+static void
+emit_malformed_error (IrcServer *server, const char *msg)
+{
+ GError *error = NULL;
+
+ g_set_error (&error,
+ IRC_ERROR,
+ IRC_ERROR_MALFORMED_MESSAGE,
+ "Server %s sent an unparsable message (%s) "
+ "(This is most likely a bug in ChatterBox)",
+ server->hostname,
+ msg);
+
+ server_emit_error (server, error);
+
+ g_error_free (error);
+}
+
+static gboolean
+process_data (IrcServer *server)
+{
+ char *crlf = strstr (server->unparsed->str, CRLF);
+
+ if (!crlf && server->unparsed->len > 510)
+ {
+ /* We have seen 510 bytes in a row without
+ * a CRLF. This means the server is sending
+ * garbage
+ */
+ emit_malformed_error (server, "No CRLF");
+ lac_connection_close (server->connection);
+ return FALSE;
+ }
+ else if (crlf)
+ {
+ IrcMessage *msg;
+ const char *s = server->unparsed->str;
+ int len = server->unparsed->len;
+
+ msg = message_new_parsed (&s, &len);
+
+ if (!msg)
+ {
+ emit_malformed_error (server, "parse error");
+ lac_connection_close (server->connection);
+ return FALSE;
+ }
+
+ g_string_erase (server->unparsed, 0, server->unparsed->len - len);
+
+ /* process message */
+#if 0
+ g_print ("\ncommand: %s - %d ", msg->prefix? msg->prefix : "", msg->code);
+ for (list = msg->parameters->head; list != NULL; list = list->next)
+ {
+ g_print ("%s ", (char *)list->data);
+ }
+ g_print ("\n");
+#endif
+
+ message_free (msg);
+ return TRUE;
+ }
+ else
+ {
+ g_print ("no data/not enough data\n");
+ /* No CRLF and less than 510 bytes: not enough
+ * data yet
+ */
+ return FALSE;
+ }
+}
+
+static void
+server_handle_read (IrcServer *server,
+ const LacConnectionReadEvent *read)
+{
+ int i;
+
+ for (i = 0; i < read->len; ++i)
+ g_print ("%c", read->data[i]);
+
+ g_string_append_len (server->unparsed, read->data, read->len);
+
+ while (process_data (server))
+ ;
+}
+
+static void
+server_handle_disconnect (IrcServer *server)
+{
+ server->state = NOT_CONNECTED;
+ lac_connection_unref (server->connection);
+ server->connection = NULL;
+}
+
+static void
+server_send_command (IrcServer *server,
+ const char *prefix,
+ ReplyCode code,
+ const char *param,
+ ...)
+{
+ va_list var_args;
+ GQueue *params = g_queue_new ();
+ const char *command;
+ GList *list;
+ GString *message;
+
+ command = code_to_command (code);
+
+ g_return_if_fail (command != NULL);
+
+ va_start (var_args, param);
+
+ while (param != NULL)
+ {
+ g_queue_push_tail (params, (gpointer)param);
+
+ param = va_arg (var_args, char *);
+ }
+
+ va_end (var_args);
+
+ message = g_string_new ("");
+
+ if (prefix)
+ {
+ g_string_append (message, ":");
+ g_string_append (message, prefix);
+ g_string_append (message, " ");
+ }
+
+ g_string_append (message, command);
+
+ for (list = params->head; list != NULL; list = list->next)
+ {
+ g_string_append (message, " ");
+ if (!list->next)
+ g_string_append (message, ":");
+ g_string_append (message, list->data);
+ }
+
+ g_string_append (message, CRLF);
+
+ lac_connection_write (server->connection, message->str, message->len);
+
+ g_string_free (message, TRUE);
+}
+
+static void
+server_send_registration (IrcServer *server)
+{
+ server_send_command (server, NULL, CMD_PASS, server->password, NULL);
+ server_send_command (server, NULL, CMD_NICK, server->nickname, NULL);
+ server_send_command (
+ server, NULL, CMD_USER,
+ server->local_user_name? server->local_user_name : "unknown",
+ server->local_host_name? server->local_host_name : "unknown",
+ server->local_domain_name? server->local_domain_name : "unknown",
+ server->real_name? server->real_name : "Unknown",
+ NULL);
+}
+
+static void
+connection_callback (LacConnection *connection,
+ const LacConnectionEvent *event)
+{
+ IrcServer *server = lac_connection_get_data (connection);
+
+ g_assert (server);
+
+ switch (event->type)
+ {
+ case LAC_CONNECTION_EVENT_CONNECT: /* connect successful */
+ server_emit_connect (server);
+ server->state = CONNECTED;
+ server_send_registration (server);
+ break;
+
+ case LAC_CONNECTION_EVENT_READ:
+ server_handle_read (server, &(event->read));
+ break; /* data to read */
+
+ case LAC_CONNECTION_EVENT_CLOSE: /* Connection closed */
+ if (event->close.remote_closed)
+ server_emit_close (server);
+ server_handle_disconnect (server);
+ break;
+
+ case LAC_CONNECTION_EVENT_ERROR: /* error */
+ server_emit_error (server, event->error.err);
+ server_handle_disconnect (server);
+ break;
+ }
+}
+
+static void
+address_callback (const LacAddress * address,
+ gpointer data,
+ const GError * err)
+{
+ IrcServer *server = data;
+
+ if (err)
+ {
+ server_emit_error (server, err);
+ }
+ else
+ {
+ server_emit_dns_complete (server);
+ server->connection = lac_connection_new (
+ address, server->port, connection_callback, server);
+ }
+}
+
+static char *
+generate_password (void)
+{
+ return g_strdup_printf ("CH%x%x%x",
+ g_random_int(),
+ g_random_int(),
+ g_random_int());
+}
+
+void
+irc_server_connect (IrcServer *server,
+ const char *nick,
+ const char *password)
+{
+ char *orig_pw, *orig_nick;
+
+ g_return_if_fail (server != NULL);
+ g_return_if_fail (server->state == NOT_CONNECTED);
+ g_return_if_fail (nick != NULL);
+
+ if (server->state != NOT_CONNECTED)
+ return;
+
+ orig_pw = server->password;
+ orig_nick = server->nickname;
+
+ if (password)
+ server->password = g_strdup (password);
+ else
+ server->password = generate_password ();
+
+ server->nickname = g_strdup (nick);
+
+ if (orig_pw)
+ g_free (orig_pw);
+ if (orig_nick)
+ g_free (orig_nick);
+
+ server->state = CONNECTING;
+ lac_address_new_lookup_from_name (
+ server->hostname, address_callback, server);
+}
+
+void
+irc_server_disconnect (IrcServer *server)
+{
+}
+
+/* Tracing memory
+ */
+
+static gpointer
+trace (gpointer data,
+ gpointer free_func,
+ GList **free_list)
+{
+ *free_list = g_list_prepend (*free_list, data);
+ *free_list = g_list_prepend (*free_list, free_func);
+ return data;
+}
+
+GQuark
+irc_error_quark (void)
+{
+ static GQuark q = 0;
+
+ if (q == 0)
+ q = g_quark_from_static_string ("irc-error-quark");
+
+ return q;
+}
+