diff options
Diffstat (limited to 'irc.c')
-rw-r--r-- | irc.c | 931 |
1 files changed, 931 insertions, 0 deletions
@@ -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; +} + |