]> Pileus Git - ~andy/lamechat/blobdiff - xmpp.c
XMPP MUC
[~andy/lamechat] / xmpp.c
diff --git a/xmpp.c b/xmpp.c
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0656b9264f11916b66277cd2fefede52477d997d 100644 (file)
--- a/xmpp.c
+++ b/xmpp.c
@@ -0,0 +1,490 @@
+/*
+ * 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/>.
+ */
+
+#define _GNU_SOURCE
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <netdb.h>
+#include <expat.h>
+
+#include "util.h"
+#include "chat.h"
+#include "conf.h"
+#include "net.h"
+
+/* Constants */
+#define AUTH_LEN 512
+
+/* XMPP types */
+typedef enum {
+       XMPP_DEAD,
+       XMPP_SEND_STREAM,
+       XMPP_RECV_FEATURES,
+       XMPP_SEND_STARTTLS,
+       XMPP_RECV_PROCEED,
+       XMPP_SEND_AUTH,
+       XMPP_RECV_SUCCESS,
+       XMPP_SEND_BIND,
+       XMPP_RECV_JID,
+       XMPP_PRESENCE,
+       XMPP_ENCRYPT,
+       XMPP_RESTART,
+       XMPP_READY,
+} xmpp_state_t;
+
+typedef struct xmpp_server_t {
+       const char  *name;
+       const char  *protocol;
+       int          connect;
+       const char  *host;
+       int          port;
+       const char  *muc;
+       const char  *nick;
+       const char  *jid;
+       const char  *user;
+       const char  *pass;
+
+       net_t        net;
+       XML_Parser   expat;
+       xmpp_state_t state;
+       buf_t        buf;
+       int          indent;
+
+       int          in_error;
+
+       struct xmpp_server_t *next;
+} xmpp_server_t;
+
+typedef struct xmpp_channel_t {
+       const char  *name;
+       const char  *channel;
+       const char  *server;
+       int          join;
+       struct xmpp_channel_t *next;
+} xmpp_channel_t;
+
+/* Local data */
+static xmpp_server_t  *servers;
+static xmpp_channel_t *channels;
+
+/* Local functions */
+const char *find_attr(const char **attrs, const char *name)
+{
+       for (int i = 0; attrs[i] && attrs[i+1]; i += 2)
+               if (match(attrs[i+0], name))
+                       return attrs[i+1];
+       return NULL;
+}
+
+static xmpp_server_t *find_server(const char *name, int create)
+{
+       xmpp_server_t *cur = NULL, *last = NULL;
+       for (cur = servers; cur; last = cur, cur = cur->next)
+               if (match(cur->name, name))
+                       break;
+       if (!cur && create) {
+               cur = new0(xmpp_server_t);
+               cur->name = get_name(name);
+               if (last)
+                       last->next = cur;
+               else
+                       servers = cur;
+       }
+       return cur;
+}
+
+static xmpp_channel_t *find_channel(const char *name, int create)
+{
+       xmpp_channel_t *cur = NULL, *last = NULL;
+       for (cur = channels; cur; last = cur, cur = cur->next)
+               if (match(cur->name, name))
+                       break;
+       if (!cur && create) {
+               cur = new0(xmpp_channel_t);
+               cur->name = get_name(name);
+               if (last)
+                       last->next = cur;
+               else
+                       channels = cur;
+       }
+       return cur;
+}
+
+static void on_start(void *_srv, const char *tag, const char **attrs)
+{
+       xmpp_server_t *srv = _srv;
+
+       /* Debug print */
+       debug("%*s<%s>",
+               srv->indent*4, "", tag);
+       for (int i = 0; attrs[i] && attrs[i+1]; i += 2) {
+               debug("%*s%s=\"%s\"%s",
+                       srv->indent*4+8, "",
+                       attrs[i+0], attrs[i+1],
+                       attrs[i+2] ? "" : ">");
+       }
+       srv->indent++;
+
+       /* Start TLS */
+       if (srv->state == XMPP_RECV_FEATURES) {
+               if (match(tag, "starttls")) {
+                       debug("xmpp: features -> starttls");
+                       srv->state = XMPP_SEND_STARTTLS;
+               }
+               if (match(tag, "mechanisms")) {
+                       debug("xmpp: features -> auth");
+                       srv->state = XMPP_SEND_AUTH;
+               }
+               if (match(tag, "bind")) {
+                       debug("xmpp: features -> bind");
+                       srv->state = XMPP_SEND_BIND;
+               }
+       }
+       if (srv->state == XMPP_RECV_PROCEED) {
+               if (match(tag, "proceed")) {
+                       debug("xmpp: proceed -> encrypt");
+                       srv->state = XMPP_ENCRYPT;
+               }
+       }
+
+       /* Authentication */
+       if (srv->state == XMPP_RECV_SUCCESS) {
+               if (match(tag, "success")) {
+                       debug("xmpp: success -> restart");
+                       srv->state = XMPP_RESTART;
+               }
+       }
+       if (srv->state == XMPP_RECV_JID) {
+               if (match(tag, "jid")) {
+                       debug("xmpp: jid -> presence");
+                       srv->state = XMPP_PRESENCE;
+               }
+       }
+
+       /* Info queries */
+       if (srv->state == XMPP_READY) {
+               if (match(tag, "item"))
+                       chat_notice(NULL, NULL, "item: [%s] %s",
+                                       find_attr(attrs, "jid"),
+                                       find_attr(attrs, "name"));
+               if (match(tag, "identity"))
+                       chat_notice(NULL, NULL, "identity: %s",
+                                       find_attr(attrs, "name"));
+               if (match(tag, "feature"))
+                       chat_notice(NULL, NULL, "feature: %s",
+                                       find_attr(attrs, "var"));
+       }
+
+       /* Error handling */
+       if (match(tag, "stream:error"))
+               srv->in_error = 1;
+}
+
+static void on_end(void *_srv, const char *tag)
+{
+       xmpp_server_t *srv = _srv;
+       const char *data = srv->buf.data;
+
+       /* Debug print */
+       if (srv->buf.len) {
+               debug("%*s \"%s\"",
+                       srv->indent*4, "",
+                       (char*)srv->buf.data);
+               srv->buf.len = 0;
+       }
+       srv->indent--;
+
+       /* Receive messages */
+       if (srv->state == XMPP_READY) {
+               if (match(tag, "body")) {
+                       debug("xmpp: jid -> ready");
+                       chat_recv("#test", NULL, data);
+               }
+       }
+
+       /* Error handling */
+       if (match(tag, "stream:error"))
+               srv->in_error = 0;
+       if (srv->in_error) {
+               if (match(tag, "text")) {
+                       debug("xmpp: error: %s", data);
+                       chat_notice(NULL, NULL, "error: %s", data);
+               }
+       }
+}
+
+static void on_data(void *_srv, const char *data, int len)
+{
+       xmpp_server_t *srv = _srv;
+
+       /* Debug print */
+       append(&srv->buf, data, len);
+}
+
+static void on_recv(void *_srv, char *buf, int len)
+{
+       static char plain[AUTH_LEN];
+       static char coded[AUTH_LEN];
+       const char *resource;
+
+       xmpp_server_t *srv = _srv;
+
+       /* Parse input */
+       if (len > 0)
+               XML_Parse(srv->expat, buf, len, 0);
+
+       /* State machine */
+output:
+       if (srv->state == XMPP_SEND_STREAM) {
+               if (net_print(&srv->net,
+                   "<?xml version='1.0'?>"
+                   "<stream:stream"
+                   " from='%s'"
+                   " to='%s'"
+                   " version='1.0'"
+                   " xml:lang='en'"
+                   " xmlns='jabber:client'"
+                   " xmlns:stream='http://etherx.jabber.org/streams'>",
+                   srv->jid, srv->host)) {
+                       debug("xmpp: stream -> features");
+                       srv->state = XMPP_RECV_FEATURES;
+               }
+       }
+       if (srv->state == XMPP_SEND_STARTTLS) {
+               if (net_print(&srv->net,
+                   "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")) {
+                       debug("xmpp: startls -> proceed");
+                       srv->state = XMPP_RECV_PROCEED;
+               }
+       }
+       if (srv->state == XMPP_SEND_AUTH) {
+               len = snprintf(plain, AUTH_LEN, "%s%c%s%c%s",
+                               srv->user, '\0', srv->user, '\0', srv->pass);
+               len = base64(plain, len, coded, AUTH_LEN);
+               if (net_print(&srv->net,
+                   "<auth"
+                   " xmlns='urn:ietf:params:xml:ns:xmpp-sasl'"
+                   " mechanism='PLAIN'>%.*s</auth>",
+                       len, coded)) {
+                       debug("xmpp: auth -> success");
+                       srv->state = XMPP_RECV_SUCCESS;
+               }
+       }
+       if (srv->state == XMPP_SEND_BIND) {
+               resource = srv->jid;
+               while (*resource && *resource != '/')
+                       resource++;
+               while (*resource && *resource == '/')
+                       resource++;
+               if (net_print(&srv->net,
+                   "<iq id='bind' type='set'>"
+                   "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>"
+                   "<resource>%s</resource>"
+                   "</bind>"
+                   "</iq>",
+                       resource)) {
+                       debug("xmpp: bind -> jid");
+                       srv->state = XMPP_RECV_JID;
+               }
+       }
+       if (srv->state == XMPP_PRESENCE) {
+               for (xmpp_channel_t *chan = channels; chan; chan = chan->next) {
+                       if (!chan->join)
+                               continue;
+                       net_print(&srv->net,
+                               "<presence id='join' from='%s' to='%s@%s/%s'>"
+                               "<x xmlns='http://jabber.org/protocol/muc'/>"
+                               "</presence>",
+                               srv->jid, chan->channel, srv->muc, srv->nick);
+               }
+               srv->state = XMPP_READY;
+       }
+       if (srv->state == XMPP_ENCRYPT) {
+               /* Encrypt connection */
+               net_encrypt(&srv->net);
+
+               /* Reset Expat */
+               if (!(XML_ParserReset(srv->expat, NULL)))
+                       error("Error resetting XML parser");
+               XML_SetUserData(srv->expat, srv);
+               XML_SetStartElementHandler(srv->expat, on_start);
+               XML_SetEndElementHandler(srv->expat, on_end);
+               XML_SetCharacterDataHandler(srv->expat, on_data);
+
+               /* Reset server */
+               debug("xmpp: encrypt -> stream");
+               srv->state = XMPP_SEND_STREAM;
+               goto output;
+       }
+       if (srv->state == XMPP_RESTART) {
+               /* Reset Expat */
+               if (!(XML_ParserReset(srv->expat, NULL)))
+                       error("Error resetting XML parser");
+               XML_SetUserData(srv->expat, srv);
+               XML_SetStartElementHandler(srv->expat, on_start);
+               XML_SetEndElementHandler(srv->expat, on_end);
+               XML_SetCharacterDataHandler(srv->expat, on_data);
+
+               /* Reset server */
+               debug("xmpp: restart -> stream");
+               srv->state = XMPP_SEND_STREAM;
+               goto output;
+       }
+}
+
+static void xmpp_connect(xmpp_server_t *srv)
+{
+       /* Net connect */
+       srv->net.recv = on_recv;
+       srv->net.data = srv;
+       net_open(&srv->net, srv->host, srv->port);
+
+       /* Setup Expat */
+       if (!(srv->expat = XML_ParserCreate(NULL)))
+               error("Error creating XML parser");
+       XML_SetUserData(srv->expat, srv);
+       XML_SetStartElementHandler(srv->expat, on_start);
+       XML_SetEndElementHandler(srv->expat, on_end);
+       XML_SetCharacterDataHandler(srv->expat, on_data);
+
+       /* Setup server */
+       srv->state = XMPP_SEND_STREAM;
+}
+
+/* XMPP functions */
+void xmpp_init(void)
+{
+       for (xmpp_server_t *cur = servers; cur; cur = cur->next) {
+               if (!match(cur->protocol, "xmpp"))
+                       continue;
+               if (!cur->port)
+                       cur->port = 5222;
+               if (!cur->jid)
+                       error("jid is required");
+               if (cur->connect)
+                       xmpp_connect(cur);
+       }
+}
+
+void xmpp_config(const char *group, const char *name, const char *key, const char *value)
+{
+       xmpp_server_t  *srv;
+       xmpp_channel_t *chan;
+
+       if (match(group, "server")) {
+               srv = find_server(name, 1);
+               if (match(key, "protocol") &&
+                   match(value, "xmpp"))
+                       srv->protocol = get_string(value);
+               if (match(srv->protocol, "xmpp")) {
+                       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, "muc"))
+                               srv->muc = get_string(value);
+                       else if (match(key, "nick"))
+                               srv->nick = get_string(value);
+                       else if (match(key, "jid"))
+                               srv->jid = get_string(value);
+                       else if (match(key, "user"))
+                               srv->user = get_string(value);
+                       else if (match(key, "pass"))
+                               srv->pass = get_string(value);
+               }
+       } else if (match(group, "channel")) {
+               chan = find_channel(name, 1);
+               if (match(key, "server") &&
+                   find_server(value, 0))
+                       chan->server = get_string(value);
+               if (chan->server) {
+                       if (match(key, "channel"))
+                               chan->channel = get_string(value);
+                       else if (match(key, "join"))
+                               chan->join = get_bool(value);
+               }
+       }
+}
+
+void xmpp_send(const char *channel, const char *msg)
+{
+       const char *arg;
+
+       xmpp_channel_t *chan = find_channel(channel, 0);
+       if (!chan)
+               return;
+       xmpp_server_t *srv = find_server(chan->server, 0);
+       if (!srv || !srv->protocol)
+               return;
+
+       /* Handle commands */
+       if (msg[0] == '/') {
+               if (prefix(msg, "/items", &arg)) {
+                       net_print(&srv->net,
+                               "<iq id='items' type='get' from='%s' to='%s'>"
+                               "<query xmlns='http://jabber.org/protocol/disco#items' />"
+                               "</iq>",
+                               srv->jid, arg ?: srv->host);
+               }
+               else if (prefix(msg, "/info", &arg)) {
+                       net_print(&srv->net,
+                               "<iq id='info' type='get' from='%s' to='%s'>"
+                               "<query xmlns='http://jabber.org/protocol/disco#info' />"
+                               "</iq>",
+                               srv->jid, arg ?: srv->host);
+               }
+               else if (prefix(msg, "/list", &arg)) {
+                       net_print(&srv->net,
+                               "<iq id='list' type='get' from='%s' to='%s'>"
+                               "<query xmlns='http://jabber.org/protocol/disco#items' />"
+                               "</iq>",
+                               srv->jid, arg ?: srv->muc);
+               }
+               else if (prefix(msg, "/join", &arg)) {
+                       if (!arg) {
+                               chat_notice(NULL, NULL, "usage: /join <channel>");
+                               return;
+                       }
+                       net_print(&srv->net,
+                               "<presence id='join' from='%s' to='%s@%s/%s'>"
+                               "<x xmlns='http://jabber.org/protocol/muc'/>"
+                               "</presence>",
+                               srv->jid, arg, srv->muc, srv->nick);
+               }
+               else {
+                       debug("unknown: [%s]", msg);
+                       chat_notice(NULL, NULL,
+                               "unknown command %s", msg);
+               }
+       } else {
+               debug("message: [%s]", msg);
+               net_print(&srv->net,
+                       "<message to='%s'><body>%s</body></message>",
+                       "andy@pileus.org", msg);
+       }
+}
+
+void xmpp_exit(void)
+{
+}