/* * 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 . */ #define _GNU_SOURCE #include #include #include #include #include #include #include "util.h" #include "conf.h" #include "chat.h" #include "net.h" /* Constants */ #define AUTH_LEN 512 #define JID_LEN 256 /* 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 { server_t server; 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; char msg_from[JID_LEN]; char msg_usr[JID_LEN]; char msg_srv[JID_LEN]; char msg_res[JID_LEN]; } xmpp_server_t; typedef struct { channel_t channel; char dest[JID_LEN]; const char *room; int join; } xmpp_channel_t; /* Helper functions */ static void split_jid(const char *jid, char *usr, char *srv, char *res) { char *ptr = usr; int pos = 0; if (usr) usr[0] = '\0'; if (srv) srv[0] = '\0'; if (res) res[0] = '\0'; for (int i = 0; jid && jid[i]; i++) { switch (jid[i]) { case '@': ptr = srv; pos = 0; continue; case '/': ptr = res; pos = 0; continue; } if (ptr && (pos+1) < JID_LEN) { ptr[pos++] = jid[i]; ptr[pos] = '\0'; } } debug("JID: '%s' usr=[%s] srv=[%s] res=[%s]", jid, usr, srv, res); } static xmpp_channel_t *find_dest(xmpp_server_t *srv, const char *jid, int is_muc) { static char jid_usr[JID_LEN]; static char jid_srv[JID_LEN]; static char dest[JID_LEN]; xmpp_channel_t *chan; split_jid(jid, jid_usr, jid_srv, NULL); snprintf(dest, JID_LEN, "%s@%s", jid_usr, jid_srv[0] ? jid_srv : is_muc ? srv->muc : srv->host); /* Find existing channels */ for (channel_t *cur = channels; cur; cur = cur->next) { if (cur->server != &srv->server) continue; chan = (xmpp_channel_t *)cur; if (match(chan->dest, dest)) return chan; } /* Create a new channel */ chan = (xmpp_channel_t *)add_channel(jid_usr, &srv->server); strncpy(chan->dest, dest, JID_LEN); return chan; } static 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; } /* Callback functions */ 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")); } /* Receive messages */ if (srv->state == XMPP_READY) { if (match(tag, "message")) { strncpy(srv->msg_from, find_attr(attrs,"from"), JID_LEN); split_jid(srv->msg_from, srv->msg_usr, srv->msg_srv, srv->msg_res); } } /* 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; xmpp_channel_t *chan = NULL; 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, "message")) { srv->msg_from[0] = '\0'; srv->msg_usr[0] = '\0'; srv->msg_srv[0] = '\0'; srv->msg_res[0] = '\0'; } if (match(tag, "body")) { debug("xmpp: jid -> ready"); if (match(srv->msg_srv, srv->muc)) { chan = find_dest(srv, srv->msg_from, 1); chat_recv(&chan->channel, srv->msg_res, data); } else { chan = find_dest(srv, srv->msg_from, 0); chat_recv(&chan->channel, srv->msg_usr, 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); /* Stream restart */ 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; } 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; } /* State machine */ if (srv->state == XMPP_SEND_STREAM) { if (net_print(&srv->net, "" "", srv->jid, srv->host)) { debug("xmpp: stream -> features"); srv->state = XMPP_RECV_FEATURES; } } if (srv->state == XMPP_SEND_STARTTLS) { if (net_print(&srv->net, "")) { 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, "%.*s", 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, "" "" "%s" "" "", resource)) { debug("xmpp: bind -> jid"); srv->state = XMPP_RECV_JID; } } if (srv->state == XMPP_PRESENCE) { for (channel_t *cur = channels; cur; cur = cur->next) { xmpp_channel_t *chan = (xmpp_channel_t*)cur; if (cur->server != &srv->server || !chan->join) continue; net_print(&srv->net, "" "" "", srv->jid, chan->dest, srv->nick); } srv->state = XMPP_READY; } } 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 (server_t *cur = servers; cur; cur = cur->next) { if (cur->protocol != XMPP) continue; xmpp_server_t *srv = (xmpp_server_t*)cur; if (!srv->port) srv->port = 5222; if (!srv->jid) error("jid is required"); if (srv->connect) xmpp_connect(srv); } for (channel_t *cur = channels; cur; cur = cur->next) { if (cur->server->protocol != XMPP) continue; xmpp_channel_t *chan = (xmpp_channel_t*)cur; xmpp_server_t *srv = (xmpp_server_t*)cur->server; if (!chan->room) chan->room = strcopy(cur->name); snprintf(chan->dest, JID_LEN, "%s@%s", chan->room, srv->muc); } } server_t *xmpp_server(void) { return new0(xmpp_server_t); } channel_t *xmpp_channel(void) { return new0(xmpp_channel_t); } void xmpp_config(server_t *server, channel_t *channel, const char *group, const char *name, const char *key, const char *value) { xmpp_server_t *srv = (xmpp_server_t*)server; xmpp_channel_t *chan = (xmpp_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, "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); } if (channel) { if (match(key, "room")) chan->room = get_string(value); else if (match(key, "join")) chan->join = get_bool(value); } } void xmpp_send(message_t *msg) { xmpp_channel_t *chan = (xmpp_channel_t*)msg->channel; xmpp_server_t *srv = (xmpp_server_t*)msg->channel->server; const char *txt = msg->text; const char *arg; /* Handle commands */ if (txt[0] == '/') { if (prefix(txt, "/items", &arg)) { net_print(&srv->net, "" "" "", srv->jid, arg ?: srv->host); } else if (prefix(txt, "/info", &arg)) { net_print(&srv->net, "" "" "", srv->jid, arg ?: srv->host); } else if (prefix(txt, "/list", &arg)) { net_print(&srv->net, "" "" "", srv->jid, arg ?: srv->muc); } else if (prefix(txt, "/join", &arg)) { if (!arg) { chat_notice(NULL, NULL, "usage: /join "); return; } chan = find_dest(srv, arg, 1); net_print(&srv->net, "" "" "", srv->jid, chan->dest, srv->nick); } else if (prefix(txt, "/query", &arg)) { if (!arg) { chat_notice(NULL, NULL, "usage: /query "); return; } chan = find_dest(srv, arg, 0); (void)chan; } else { debug("unknown: [%s]", txt); chat_notice(NULL, NULL, "unknown command %s", txt); } } else { debug("message: [%s]", txt); if (chan->room) { net_print(&srv->net, "" "%s" "", srv->jid, chan->dest, txt); } else { net_print(&srv->net, "" "%s" "", srv->jid, chan->dest, txt); } } } void xmpp_exit(void) { }