+/*
+ * Copyright (C) 2017 Andy Spencer <andy753421@gmail.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <ctype.h>
+
+#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 {
+ user_t user;
+
+ const char *nick;
+} irc_user_t;
+
+typedef struct {
+ channel_t channel;
+
+ const char *dest;
+ int join;
+} irc_channel_t;
+
+typedef struct {
+ server_t server;
+
+ int connect;
+ int noverify;
+ const char *host;
+ int port;
+ int tls;
+ const char *nick;
+ const char *auth;
+ const char *pass;
+
+ irc_user_t myself;
+ 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 = new0(irc_channel_t);
+ chan->channel.server = &srv->server;
+ chan->channel.name = strcopy(dest);
+ chan->dest = strcopy(dest);
+ add_channel(&chan->channel);
+ return chan;
+}
+
+static irc_user_t *find_nick(irc_server_t *srv, const char *from, int create)
+{
+ irc_user_t *usr;
+
+ /* Find existing channels */
+ for (user_t *cur = users; cur; cur = cur->next) {
+ if (cur->server != &srv->server)
+ continue;
+ usr = (irc_user_t *)cur;
+ if (match(usr->nick, from))
+ return usr;
+ }
+
+ /* Create a new channel */
+ usr = new0(irc_user_t);
+ usr->user.server = &srv->server;
+ usr->user.name = strcopy(from);
+ usr->nick = strcopy(from);
+ add_user(&usr->user);
+ return usr;
+}
+
+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->noverify ? NET_NOVERIFY : 0);
+ 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;
+ irc_user_t *usr;
+ 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);
+ usr = find_nick(srv, from, 1);
+ chat_recv(&chan->channel, &usr->user, msg);
+ }
+ if (match(cmd, "PRIVMSG") && dst[0] != '#') {
+ chan = find_dest(srv, from, 1);
+ usr = find_nick(srv, from, 1);
+ chat_recv(&chan->channel, &usr->user, 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);
+ }
+}
+
+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 (match(group, "server")) {
+ if (match(key, "protocol")) {
+ irc_server_t *srv = new0(irc_server_t);
+ srv->server.protocol = IRC;
+ srv->server.name = strcopy(get_name(name));
+ add_server(&srv->server);
+ }
+ else if (match(key, "connect"))
+ srv->connect = get_bool(value);
+ else if (match(key, "noverify"))
+ srv->noverify = 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 (match(group, "channel")) {
+ if (match(key, "server")) {
+ irc_channel_t *chan = new0(irc_channel_t);
+ chan->channel.server = &srv->server;
+ chan->channel.name = strcopy(get_name(name));
+ add_channel(&chan->channel);
+ }
+ else 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 <user>");
+ 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->myself.user, text);
+ }
+ }
+}
+
+void irc_exit(void)
+{
+}
+