/* * Copyright (C) 2017 Andy Spencer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include "util.h" #include "conf.h" #include "chat.h" #include "net.h" /* IRC constants */ #define IRC_LINE 512 /* IRC types */ typedef enum { IRC_CONNECT, IRC_ENCRYPT, IRC_SEND_TLS, IRC_RECV_TLS, IRC_SEND_STARTTLS, IRC_RECV_STARTTLS, IRC_SEND_SASL, IRC_RECV_SASL, IRC_SEND_PLAIN, IRC_SEND_AUTH, IRC_RECV_STATUS, IRC_SEND_END, IRC_SEND_USER, IRC_SEND_NICK, IRC_RECV_WELCOME, IRC_JOIN, IRC_READY, IRC_DEAD, } irc_state_t; typedef struct { channel_t channel; const char *dest; int join; } irc_channel_t; typedef struct { server_t server; int connect; const char *host; int port; int tls; const char *nick; const char *auth; const char *pass; irc_channel_t system; net_t net; irc_state_t state; char line[IRC_LINE]; int pos; } irc_server_t; typedef struct { char src[IRC_LINE]; // (:([^ ]+) +)? char cmd[IRC_LINE]; // (([A-Z0-9]+) +) char dst[IRC_LINE]; // (([^ ]+)[= ]+)? char arg[IRC_LINE]; // (([^: ]+) *)? char msg[IRC_LINE]; // (:(.*))? char from[IRC_LINE]; } irc_command_t; /* Local functions */ static void srv_notice(irc_server_t *srv, const char *fmt, ...) { static char buf[1024]; va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); chat_recv(&srv->system.channel, NULL, buf); } static void chan_notice(irc_channel_t *chan, const char *fmt, ...) { static char buf[1024]; va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); chat_recv(&chan->channel, NULL, buf); } static irc_channel_t *find_dest(irc_server_t *srv, const char *dest, int create) { irc_channel_t *chan; /* Find existing channels */ for (channel_t *cur = channels; cur; cur = cur->next) { if (cur->server != &srv->server) continue; chan = (irc_channel_t *)cur; if (match(chan->dest, dest)) return chan; } /* Create a new channel */ chan = (irc_channel_t *)add_channel(dest, &srv->server); chan->dest = strcopy(dest); return chan; } static void join_channel(irc_server_t *srv, irc_channel_t *chan) { chan_notice(chan, "Joining Channel: %s", chan->channel.name); net_print(&srv->net, "JOIN %s\n", chan->dest); net_print(&srv->net, "TOPIC %s\n", chan->dest); net_print(&srv->net, "WHO %s\n", chan->dest); } static void part_channel(irc_server_t *srv, irc_channel_t *chan) { chan_notice(chan, "Leaving Channel: %s", chan->channel.name); net_print(&srv->net, "PART %s\n", chan->dest); } static void parse_line(irc_server_t *srv, const char *line, irc_command_t *_cmd) { int i; const char *c = line; char *src = _cmd->src; char *cmd = _cmd->cmd; char *dst = _cmd->dst; char *arg = _cmd->arg; char *msg = _cmd->msg; char *from = _cmd->from; /* Clear strings */ src[0] = cmd[0] = dst[0] = arg[0] = msg[0] = from[0] = '\0'; /* Read src */ if (*c == ':') { c++; for (i = 0; *c && *c != ' '; i++) src[i] = *c++; while (*c == ' ') c++; src[i] = '\0'; for (i = 0; src[i] && src[i] != '!' && src[i] != ' '; i++) from[i] = src[i]; from[i] = '\0'; } /* Read cmd */ for (i = 0; isalnum(*c); i++) cmd[i] = *c++; while (*c == ' ') c++; cmd[i] = '\0'; /* Read dst */ if ((*c && *c != ' ') && (strchr(c+1, ' ') || strchr(c+1, '='))) { for (i = 0; *c && *c != ' '; i++) dst[i] = *c++; while (*c == '=' || *c == ' ') c++; dst[i] = '\0'; } /* Read arg */ if (*c && *c != ':' && *c != ' ') { for (i = 0; *c && *c != ':'; i++) arg[i] = *c++; while (*c == ' ') c++; do arg[i--] = '\0'; while (i >= 0 && arg[i] == ' '); } /* Read msg */ if (*c == ':') { c++; for (i = 0; *c; i++) msg[i] = *c++; msg[i] = '\0'; } debug("got line: [%s]", line); //debug(" src %s", src); //debug(" cmd %s", cmd); //debug(" dst %s", dst); //debug(" arg %s", arg); //debug(" msg %s", msg); } /* Callback functions */ static void irc_run(irc_server_t *srv, const char *line); static void on_send(void *_srv) { irc_server_t *srv = _srv; irc_run(srv, ""); } static void on_recv(void *_srv, char *buf, int len) { irc_server_t *srv = _srv; for (int i = 0; i < len; i++) { if (srv->pos < IRC_LINE) { srv->line[srv->pos] = buf[i]; srv->pos++; } if (buf[i] == '\n' || buf[i] == '\r') { srv->line[srv->pos-1] = '\0'; if (srv->pos > 1) irc_run(srv, srv->line); srv->pos = 0; } } irc_run(srv, ""); } static void on_err(void *_srv, int errno) { irc_server_t *srv = _srv; irc_run(srv, ""); } /* IRC State machine */ static void irc_run(irc_server_t *srv, const char *line) { static irc_command_t _cmd; const char *cmd = _cmd.cmd; const char *dst = _cmd.dst; const char *arg = _cmd.arg; const char *msg = _cmd.msg; const char *from = _cmd.from; /* Parse line */ parse_line(srv, line, &_cmd); /* Connection Handling */ if (srv->state == IRC_CONNECT) { srv->net.send = on_send; srv->net.recv = on_recv; srv->net.err = on_err; srv->net.data = srv; srv_notice(srv, "Joining Server: %s", srv->server.name); net_open(&srv->net, srv->host, srv->port); if (srv->tls) srv->state = IRC_ENCRYPT; else srv->state = IRC_SEND_TLS; } /* Start TLS */ if (srv->state == IRC_SEND_TLS) { if (net_print(&srv->net, "CAP REQ :tls\n")) srv->state = IRC_RECV_TLS; } if (srv->state == IRC_RECV_TLS) { if (match(cmd, "CAP") && match(arg, "ACK")) { srv_notice(srv, "Start TLS proceeding"); srv->state = IRC_SEND_STARTTLS; } if (match(cmd, "CAP") && match(arg, "NAK")) { srv_notice(srv, "Start TLS unsupported"); srv->state = IRC_SEND_END; } } if (srv->state == IRC_SEND_STARTTLS) { if (net_print(&srv->net, "STARTTLS\n")) srv->state = IRC_RECV_STARTTLS; } if (srv->state == IRC_RECV_STARTTLS) { if (prefix(msg, "STARTTLS successful", NULL)) srv->state = IRC_ENCRYPT; } /* Encryption */ if (srv->state == IRC_ENCRYPT) { net_encrypt(&srv->net); srv->state = IRC_SEND_SASL; } /* SASL authentication */ if (srv->state == IRC_SEND_SASL) { if (net_print(&srv->net, "CAP REQ :sasl\n")) srv->state = IRC_RECV_SASL; } if (srv->state == IRC_RECV_SASL) { if (match(cmd, "CAP") && match(arg, "ACK")) { srv_notice(srv, "SASL auth proceeding"); srv->state = IRC_SEND_PLAIN; } if (match(cmd, "CAP") && match(arg, "NAK")) { srv_notice(srv, "SASL auth unsupported"); srv->state = IRC_SEND_END; } } if (srv->state == IRC_SEND_PLAIN) { if (net_print(&srv->net, "AUTHENTICATE PLAIN\n")) srv->state = IRC_SEND_AUTH; } if (srv->state == IRC_SEND_AUTH) { static char plain[IRC_LINE]; static char coded[IRC_LINE]; int len; len = snprintf(plain, IRC_LINE, "%s%c%s%c%s", srv->auth, '\0', srv->auth, '\0', srv->pass); len = base64(plain, len, coded, IRC_LINE); if (net_print(&srv->net, "AUTHENTICATE %.*s\n", len, coded)) srv->state = IRC_RECV_STATUS; } if (srv->state == IRC_RECV_STATUS) { if (match(cmd, "903")) { srv_notice(srv, "SASL auth succeeded"); srv->state = IRC_SEND_END; } if (match(cmd, "904") || match(cmd, "905") || match(cmd, "906") || match(cmd, "907")) { srv_notice(srv, "SASL auth failed"); srv->state = IRC_DEAD; } } if (srv->state == IRC_SEND_END) { if (net_print(&srv->net, "CAP END\n")) srv->state = IRC_SEND_USER; } /* Connection setup */ if (srv->state == IRC_SEND_USER) { if (net_print(&srv->net, "USER %s %s %s :%s\n", getenv("USER") ?: "lameuser", get_hostname(), srv->host, srv->nick)) srv->state = IRC_SEND_NICK; } if (srv->state == IRC_SEND_NICK) { if (net_print(&srv->net, "NICK %s\n", srv->nick)) srv->state = IRC_RECV_WELCOME; } if (srv->state == IRC_RECV_WELCOME) { if (match(cmd, "001") && strstr(msg, "Welcome")) srv->state = IRC_JOIN; } if (srv->state == IRC_JOIN) { for (channel_t *cur = channels; cur; cur = cur->next) { irc_channel_t *chan = (irc_channel_t*)cur; if (cur->server != &srv->server || !chan->join) continue; join_channel(srv, chan); } srv->state = IRC_READY; } /* Receive messages */ if (srv->state == IRC_READY) { irc_channel_t *chan; if (match(cmd, "PING")) { net_print(&srv->net, "PING %s\n", msg); } if (match(cmd, "TOPIC")) { chan = find_dest(srv, arg, 1); chan_notice(chan, "Topic changed to %s", msg); strset(&chan->channel.topic, msg); } if (match(cmd, "331") || match(cmd, "332")) { chan = find_dest(srv, arg, 1); chan_notice(chan, "Topic: %s", msg); strset(&chan->channel.topic, msg); } if (match(cmd, "353") && prefix(arg, "@", &arg)) { chan = find_dest(srv, arg, 1); chan_notice(chan, "Members: %s", msg); } if (match(cmd, "PRIVMSG") && dst[0] == '#') { chan = find_dest(srv, dst, 1); chat_recv(&chan->channel, from, msg); } if (match(cmd, "PRIVMSG") && dst[0] != '#') { chan = find_dest(srv, from, 1); chat_recv(&chan->channel, from, msg); } } /* Receive notices */ if (match(cmd, "NOTICE") || match(cmd, "001") || match(cmd, "372") || match(cmd, "375") || match(cmd, "376")) srv_notice(srv, "%s", msg); } /* IRC functions */ void irc_init(void) { for (server_t *cur = servers; cur; cur = cur->next) { if (cur->protocol != IRC) continue; irc_server_t *srv = (irc_server_t*)cur; srv->system.channel.server = &srv->server; srv->system.channel.name = srv->server.name; if (!srv->port) srv->port = srv->tls ? 6697 : 6667; if (!srv->nick) srv->nick = strcopy(getenv("USER")); if (!srv->nick) srv->nick = strcopy("lameuser"); if (srv->connect) irc_run(srv, ""); } for (channel_t *cur = channels; cur; cur = cur->next) { if (cur->server->protocol != IRC) continue; irc_channel_t *chan = (irc_channel_t*)cur; if (!chan->dest) chan->dest = strcopy(cur->name); } } server_t *irc_server(void) { return new0(irc_server_t); } channel_t *irc_channel(void) { return new0(irc_channel_t); } void irc_config(server_t *server, channel_t *channel, const char *group, const char *name, const char *key, const char *value) { irc_server_t *srv = (irc_server_t*)server; irc_channel_t *chan = (irc_channel_t*)channel; if (server) { if (match(key, "connect")) srv->connect = get_bool(value); else if (match(key, "host")) srv->host = get_string(value); else if (match(key, "port")) srv->port = get_number(value); else if (match(key, "tls")) srv->tls = get_bool(value); else if (match(key, "nick")) srv->nick = get_string(value); else if (match(key, "auth")) srv->auth = get_string(value); else if (match(key, "pass")) srv->pass = get_string(value); } if (channel) { if (match(key, "dest")) chan->dest = get_string(value); else if (match(key, "join")) chan->join = get_bool(value); } } void irc_send(channel_t *channel, const char *text) { irc_channel_t *chan = (irc_channel_t*)channel; irc_server_t *srv = (irc_server_t*)channel->server; const char *arg; if (text[0] == '/') { if (prefix(text, "/join", &arg)) { if (arg) chan = find_dest(srv, arg, 1); if (chan) join_channel(srv, chan); } else if (prefix(text, "/part", &arg)) { if (arg) chan = find_dest(srv, arg, 0); if (chan) part_channel(srv, chan); } else if (prefix(text, "/query", &arg)) { if (!arg) { chan_notice(chan, "usage: /query "); return; } chan = find_dest(srv, arg, 0); chan_notice(chan, "User: %s", arg); } else { chan_notice(chan, "Unknown command %s", text); } } else { if (chan == &srv->system) { chan_notice(chan, "Cannot send to server"); } else if (!chan->dest) { chan_notice(chan, "No destination for message"); } else { net_print(&srv->net, "PRIVMSG %s :%s\n", chan->dest, text); chat_recv(channel, srv->nick, text); } } } void irc_exit(void) { }