+/*
+ * 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)
+{
+}