summaryrefslogtreecommitdiff
path: root/src/rexmpp_jingle.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/rexmpp_jingle.c')
-rw-r--r--src/rexmpp_jingle.c540
1 files changed, 540 insertions, 0 deletions
diff --git a/src/rexmpp_jingle.c b/src/rexmpp_jingle.c
new file mode 100644
index 0000000..93dc628
--- /dev/null
+++ b/src/rexmpp_jingle.c
@@ -0,0 +1,540 @@
+/**
+ @file rexmpp_jingle.c
+ @brief Jingle routines
+ @author defanor <defanor@uberspace.net>
+ @date 2021
+ @copyright MIT license.
+
+The following XEPs are handled here so far:
+
+- XEP-0166: Jingle
+- XEP-0234: Jingle File Transfer
+- XEP-0261: Jingle In-Band Bytestreams Transport Method
+*/
+
+#include <string.h>
+#include <syslog.h>
+#include <errno.h>
+#include <libgen.h>
+#include <gsasl.h>
+#include <nettle/sha2.h>
+
+#include "rexmpp.h"
+#include "rexmpp_jingle.h"
+
+
+rexmpp_jingle_session_t *
+rexmpp_jingle_session_by_id (rexmpp_t *s, const char *sid) {
+ if (sid == NULL) {
+ return NULL;
+ }
+ rexmpp_jingle_session_t *cur = s->jingle;
+ while (cur != NULL) {
+ if (strcmp(cur->sid, sid) == 0) {
+ return cur;
+ }
+ cur = cur->next;
+ }
+ rexmpp_log(s, LOG_WARNING, "No Jingle session with sid %s found", sid);
+ return NULL;
+}
+
+rexmpp_jingle_session_t *
+rexmpp_jingle_session_by_ibb_sid (rexmpp_t *s, const char *ibb_sid) {
+ if (ibb_sid == NULL) {
+ return NULL;
+ }
+ rexmpp_jingle_session_t *cur = s->jingle;
+ while (cur != NULL) {
+ if (strcmp(cur->ibb_sid, ibb_sid) == 0) {
+ return cur;
+ }
+ cur = cur->next;
+ }
+ rexmpp_log(s, LOG_WARNING,
+ "No Jingle session with ibb_sid %s found", ibb_sid);
+ return NULL;
+}
+
+void rexmpp_jingle_session_destroy (rexmpp_jingle_session_t *session) {
+ if (session->jid != NULL) {
+ free(session->jid);
+ }
+ if (session->sid != NULL) {
+ free(session->sid);
+ }
+ if (session->negotiation != NULL) {
+ xmlFreeNodeList(session->negotiation);
+ }
+ if (session->f != NULL) {
+ fclose(session->f);
+ }
+ free(session);
+}
+
+void rexmpp_jingle_session_delete (rexmpp_t *s, rexmpp_jingle_session_t *sess) {
+ if (sess == NULL) {
+ return;
+ }
+ rexmpp_log(s, LOG_DEBUG, "Removing Jingle session %s", sess->sid);
+ rexmpp_jingle_session_t **next_ptr = &(s->jingle), *cur = s->jingle;
+ while (cur != NULL) {
+ if (sess == cur) {
+ *next_ptr = cur->next;
+ rexmpp_jingle_session_destroy(sess);
+ }
+ next_ptr = &(cur->next);
+ cur = cur->next;
+ }
+}
+
+void rexmpp_jingle_stop (rexmpp_t *s) {
+ while (s->jingle != NULL) {
+ rexmpp_jingle_session_delete(s, s->jingle);
+ }
+}
+
+int rexmpp_jingle_session_add (rexmpp_t *s, rexmpp_jingle_session_t *sess) {
+ uint32_t sessions_num = 0;
+ rexmpp_jingle_session_t *cur = s->jingle;
+ while (cur != NULL) {
+ sessions_num++;
+ cur = cur->next;
+ }
+ if (sessions_num >= s->max_jingle_sessions) {
+ rexmpp_log(s, LOG_ERR, "Too many Jingle sessions, discaring a new one");
+ rexmpp_jingle_session_destroy(sess);
+ return 0;
+ }
+ rexmpp_log(s, LOG_DEBUG, "Adding Jingle session %s", sess->sid);
+ sess->next = s->jingle;
+ s->jingle = sess;
+ return 1;
+}
+
+void rexmpp_jingle_session_delete_by_id (rexmpp_t *s, const char *sid) {
+ rexmpp_jingle_session_delete(s, rexmpp_jingle_session_by_id(s, sid));
+}
+
+
+void rexmpp_jingle_accept_file_cb (rexmpp_t *s,
+ void *ptr,
+ xmlNodePtr request,
+ xmlNodePtr response,
+ int success)
+{
+ (void)request;
+ (void)response;
+ char *sid = ptr;
+ if (! success) {
+ rexmpp_log(s, LOG_ERR, "Failed to accept a Jingle file transfer");
+ rexmpp_jingle_session_delete_by_id(s, sid);
+ }
+ free(sid);
+}
+
+rexmpp_err_t
+rexmpp_jingle_accept_file (rexmpp_t *s,
+ rexmpp_jingle_session_t *session,
+ const char *path)
+{
+ session->f = fopen(path, "wb");
+ if (session->f == NULL) {
+ rexmpp_log(s, LOG_ERR, "Failed to open %s for writing: %s",
+ path, strerror(errno));
+ return REXMPP_E_OTHER;
+ }
+ xmlNodePtr jingle = session->negotiation;
+ xmlNodePtr content = rexmpp_xml_find_child(jingle, "urn:xmpp:jingle:1", "content");
+
+ xmlNodePtr new_jingle = rexmpp_xml_new_node("jingle", "urn:xmpp:jingle:1");
+ xmlNewProp(new_jingle, "action", "session-accept");
+ xmlNewProp(new_jingle, "responder", s->assigned_jid.full);
+ xmlNewProp(new_jingle, "sid", session->sid);
+ xmlAddChild(new_jingle, xmlCopyNode(content, 1));
+ xmlFreeNode(session->negotiation);
+ session->negotiation = xmlCopyNode(new_jingle, 1);
+ return rexmpp_iq_new(s, "set", session->jid, new_jingle,
+ rexmpp_jingle_accept_file_cb, strdup(session->sid));
+}
+
+rexmpp_err_t
+rexmpp_jingle_accept_file_by_id (rexmpp_t *s,
+ const char *sid,
+ const char *path)
+{
+ return
+ rexmpp_jingle_accept_file(s, rexmpp_jingle_session_by_id(s, sid), path);
+}
+
+void rexmpp_jingle_session_terminate_cb (rexmpp_t *s,
+ void *ptr,
+ xmlNodePtr request,
+ xmlNodePtr response,
+ int success)
+{
+ (void)request;
+ (void)response;
+ char *sid = ptr;
+ if (! success) {
+ rexmpp_log(s, LOG_ERR, "Failed to terminate session %s, removing anyway",
+ sid);
+ }
+ rexmpp_jingle_session_delete_by_id(s, sid);
+ free(sid);
+}
+
+rexmpp_err_t
+rexmpp_jingle_session_terminate (rexmpp_t *s,
+ const char *sid,
+ xmlNodePtr reason_node,
+ const char *reason_text)
+{
+ rexmpp_jingle_session_t *session = rexmpp_jingle_session_by_id(s, sid);
+ if (session == NULL) {
+ return REXMPP_E_OTHER;
+ }
+ xmlNodePtr jingle = rexmpp_xml_new_node("jingle", "urn:xmpp:jingle:1");
+ xmlNewProp(jingle, "action", "session-terminate");
+ xmlNewProp(jingle, "sid", sid);
+ xmlNodePtr reason = rexmpp_xml_new_node("reason", "urn:xmpp:jingle:1");
+ if (reason_text != NULL) {
+ xmlNodePtr text = rexmpp_xml_new_node("text", "urn:xmpp:jingle:1");
+ xmlNodeAddContent(text, reason_text);
+ xmlAddChild(reason, text);
+ }
+ xmlAddChild(reason, reason_node);
+ xmlAddChild(jingle, reason);
+ return rexmpp_iq_new(s, "set", session->jid, jingle,
+ rexmpp_jingle_session_terminate_cb, strdup(sid));
+}
+
+rexmpp_err_t
+rexmpp_jingle_accept_file_by_sid (rexmpp_t *s,
+ const char *sid,
+ const char *path)
+{
+ rexmpp_jingle_session_t *session = rexmpp_jingle_session_by_id(s, sid);
+ if (session == NULL) {
+ return REXMPP_E_OTHER;
+ }
+ return rexmpp_jingle_accept_file(s, session, path);
+}
+
+void rexmpp_jingle_send_file_cb (rexmpp_t *s,
+ void *ptr,
+ xmlNodePtr request,
+ xmlNodePtr response,
+ int success)
+{
+ (void)request;
+ (void)response;
+ char *sid = ptr;
+ if (! success) {
+ rexmpp_log(s, LOG_ERR, "Failed to initiate file sending for sid %s", sid);
+ rexmpp_jingle_session_delete_by_id(s, sid);
+ }
+ free(sid);
+}
+
+rexmpp_err_t
+rexmpp_jingle_send_file (rexmpp_t *s,
+ const char *jid,
+ char *path)
+{
+ FILE *fh = fopen(path, "rb");
+ if (fh == NULL) {
+ rexmpp_log(s, LOG_ERR, "Failed to open %s for reading", path);
+ return REXMPP_E_OTHER;
+ }
+ char *sid = rexmpp_gen_id(s);
+ char *ibb_sid = rexmpp_gen_id(s);
+
+ xmlNodePtr jingle = rexmpp_xml_new_node("jingle", "urn:xmpp:jingle:1");
+ xmlNewProp(jingle, "action", "session-initiate");
+ xmlNewProp(jingle, "sid", sid);
+ xmlNewProp(jingle, "initiator", s->assigned_jid.full);
+
+ xmlNodePtr content = rexmpp_xml_new_node("content", "urn:xmpp:jingle:1");
+ xmlNewProp(content, "creator", "initiator");
+ xmlNewProp(content, "name", "IBB file");
+ xmlAddChild(jingle, content);
+
+ xmlNodePtr transport =
+ rexmpp_xml_new_node("transport", "urn:xmpp:jingle:transports:ibb:1");
+ xmlNewProp(transport, "block-size", "4096");
+ xmlNewProp(transport, "sid", ibb_sid);
+ xmlAddChild(content, transport);
+ xmlNodePtr description =
+ rexmpp_xml_new_node("description", "urn:xmpp:jingle:apps:file-transfer:5");
+ xmlAddChild(content, description);
+ xmlNodePtr file =
+ rexmpp_xml_new_node("file", "urn:xmpp:jingle:apps:file-transfer:5");
+ xmlAddChild(description, file);
+ xmlNodePtr file_name =
+ rexmpp_xml_new_node("name", "urn:xmpp:jingle:apps:file-transfer:5");
+ xmlNodeAddContent(file_name, basename(path));
+ xmlAddChild(file, file_name);
+
+ char buf[4096];
+
+ char hash_raw[SHA512_DIGEST_SIZE];
+ struct sha512_ctx ctx;
+ sha512_init(&ctx);
+ size_t len = fread(buf, 1, 4096, fh);
+ while (len > 0) {
+ sha512_update(&ctx, len, buf);
+ len = fread(buf, 1, 4096, fh);
+ }
+ sha512_digest(&ctx, SHA512_DIGEST_SIZE, hash_raw);
+ char *hash_base64 = NULL;
+ size_t hash_base64_len = 0;
+ gsasl_base64_to(hash_raw, SHA512_DIGEST_SIZE, &hash_base64, &hash_base64_len);
+ xmlNodePtr file_hash = rexmpp_xml_new_node("hash", "urn:xmpp:hashes:2");
+ xmlNewProp(file_hash, "algo", "sha-512");
+ xmlNodeAddContent(file_hash, hash_base64);
+ free(hash_base64);
+ xmlAddChild(file, file_hash);
+
+ long fsize = ftell(fh);
+ fseek(fh, 0, SEEK_SET);
+ snprintf(buf, 11, "%ld", fsize);
+ xmlNodePtr file_size =
+ rexmpp_xml_new_node("size", "urn:xmpp:jingle:apps:file-transfer:5");
+ xmlNodeAddContent(file_size, buf);
+ xmlAddChild(file, file_size);
+
+ rexmpp_jingle_session_t *sess = malloc(sizeof(rexmpp_jingle_session_t));
+ sess->jid = strdup(jid);
+ sess->sid = sid;
+ sess->ibb_sid = ibb_sid;
+ sess->ibb_seq = 0;
+ sess->negotiation = xmlCopyNode(jingle, 1);
+ sess->f = fh;
+ if (rexmpp_jingle_session_add(s, sess)) {
+ return rexmpp_iq_new(s, "set", sess->jid, jingle,
+ rexmpp_jingle_send_file_cb, strdup(sess->sid));
+ } else {
+ return REXMPP_E_OTHER;
+ }
+}
+
+void rexmpp_jingle_close_cb (rexmpp_t *s,
+ void *ptr,
+ xmlNodePtr request,
+ xmlNodePtr response,
+ int success)
+{
+ (void)request;
+ (void)response;
+ char *sid = ptr;
+ if (success) {
+ rexmpp_log(s, LOG_DEBUG, "Closed IBB stream for Jingle stream %s", sid);
+ } else {
+ rexmpp_log(s, LOG_ERR, "Failed to close IBB stream for Jingle stream %s", sid);
+ }
+ free(sid);
+}
+
+void rexmpp_jingle_send_cb (rexmpp_t *s,
+ void *ptr,
+ xmlNodePtr request,
+ xmlNodePtr response,
+ int success)
+{
+ (void)request;
+ (void)response;
+ char *sid = ptr;
+ if (! success) {
+ rexmpp_log(s, LOG_ERR, "An IBB stream error for Jingle sid %s", sid);
+ rexmpp_jingle_session_delete_by_id(s, sid);
+ free(sid);
+ return;
+ }
+ rexmpp_jingle_session_t *session = rexmpp_jingle_session_by_id(s, sid);
+ if (session == NULL) {
+ rexmpp_log(s, LOG_ERR, "Jingle session %s doesn't exist", sid);
+ free(sid);
+ return;
+ }
+ if (feof(session->f)) {
+ xmlNodePtr close = rexmpp_xml_new_node("close", "http://jabber.org/protocol/ibb");
+ xmlNewProp(close, "sid", session->ibb_sid);
+ rexmpp_iq_new(s, "set", session->jid, close,
+ rexmpp_jingle_close_cb, sid);
+ return;
+ } else {
+ char buf[4096];
+ size_t len = fread(buf, 1, 4096, session->f);
+ if (len > 0) {
+ xmlNodePtr data = rexmpp_xml_new_node("data", "http://jabber.org/protocol/ibb");
+ xmlNewProp(data, "sid", session->ibb_sid);
+ char *out = NULL;
+ size_t out_len = 0;
+ gsasl_base64_to(buf, len, &out, &out_len);
+ xmlNodeAddContent(data, out);
+ free(out);
+ snprintf(buf, 11, "%u", session->ibb_seq);
+ xmlNewProp(data, "seq", buf);
+ session->ibb_seq++;
+ rexmpp_iq_new(s, "set", session->jid, data,
+ rexmpp_jingle_send_cb, sid);
+ return;
+ } else {
+ rexmpp_log(s, LOG_ERR, "Failed to read from a file: %s ", strerror(errno));
+ rexmpp_jingle_session_terminate(s, sid,
+ rexmpp_xml_new_node("media-error",
+ "urn:xmpp:jingle:1"),
+ NULL);
+ }
+ }
+ free(sid);
+}
+
+int rexmpp_jingle_iq (rexmpp_t *s, xmlNodePtr elem) {
+ int handled = 0;
+ if (! s->enable_jingle) {
+ return handled;
+ }
+ xmlNodePtr jingle = rexmpp_xml_find_child(elem, "urn:xmpp:jingle:1", "jingle");
+ if (jingle != NULL) {
+ handled = 1;
+ char *action = xmlGetProp(jingle, "action");
+ char *sid = xmlGetProp(jingle, "sid");
+ char *from_jid = xmlGetProp(elem, "from");
+ if (action != NULL && sid != NULL && from_jid != NULL) {
+ if (strcmp(action, "session-initiate") == 0) {
+ /* todo: could be more than one content element, handle that */
+ xmlNodePtr content = rexmpp_xml_find_child(jingle, "urn:xmpp:jingle:1", "content");
+ if (content == NULL) {
+ rexmpp_iq_reply(s, elem, "error", rexmpp_xml_error("cancel", "bad-request"));
+ } else {
+ rexmpp_iq_reply(s, elem, "result", NULL);
+
+ xmlNodePtr description =
+ rexmpp_xml_find_child(content, "urn:xmpp:jingle:apps:file-transfer:5",
+ "description");
+ xmlNodePtr transport =
+ rexmpp_xml_find_child(content, "urn:xmpp:jingle:transports:ibb:1",
+ "transport");
+ if (description == NULL) {
+ rexmpp_jingle_session_terminate(s, sid,
+ rexmpp_xml_new_node("unsupported-applications",
+ "urn:xmpp:jingle:1"),
+ NULL);
+ } else if (transport == NULL) {
+ rexmpp_jingle_session_terminate(s, sid,
+ rexmpp_xml_new_node("unsupported-transports",
+ "urn:xmpp:jingle:1"),
+ NULL);
+ } else {
+ char *ibb_sid = xmlGetProp(transport, "sid");
+ if (ibb_sid != NULL) {
+ rexmpp_jingle_session_t *sess = malloc(sizeof(rexmpp_jingle_session_t));
+ sess->jid = strdup(from_jid);
+ sess->sid = strdup(sid);
+ sess->ibb_sid = ibb_sid;
+ sess->ibb_seq = 0;
+ sess->negotiation = xmlCopyNode(jingle, 1);
+ sess->f = NULL;
+ rexmpp_log(s, LOG_DEBUG, "Jingle session-initiate from %s, sid %s",
+ sess->jid, sid);
+ rexmpp_jingle_session_add(s, sess);
+ } else {
+ rexmpp_log(s, LOG_ERR, "Jingle IBB transport doesn't have a sid attribute");
+ rexmpp_jingle_session_terminate(s, sid,
+ rexmpp_xml_new_node("unsupported-transports",
+ "urn:xmpp:jingle:1"),
+ NULL);
+ }
+ }
+ }
+ } else if (strcmp(action, "session-terminate") == 0) {
+ /* todo: check/log the reason */
+ rexmpp_jingle_session_delete_by_id(s, sid);
+ rexmpp_iq_reply(s, elem, "result", NULL);
+ } else if (strcmp(action, "session-accept") == 0) {
+ rexmpp_iq_reply(s, elem, "result", NULL);
+ rexmpp_jingle_session_t *session = rexmpp_jingle_session_by_id(s, sid);
+ if (session != NULL) {
+ xmlNodePtr open = rexmpp_xml_new_node("open", "http://jabber.org/protocol/ibb");
+ xmlNewProp(open, "sid", session->ibb_sid);
+ xmlNewProp(open, "block-size", "4096");
+ xmlNewProp(open, "stanza", "iq");
+ rexmpp_iq_new(s, "set", session->jid, open,
+ rexmpp_jingle_send_cb, strdup(sid));
+ }
+ } else {
+ rexmpp_log(s, LOG_WARNING, "Unknown Jingle action: %s", action);
+ rexmpp_iq_reply(s, elem, "error", rexmpp_xml_error("cancel", "bad-request"));
+ }
+ } else {
+ rexmpp_log(s, LOG_WARNING, "Received a malformed Jingle element");
+ rexmpp_iq_reply(s, elem, "error", rexmpp_xml_error("cancel", "bad-request"));
+ }
+ if (action != NULL) {
+ free(action);
+ }
+ if (sid != NULL) {
+ free(sid);
+ }
+ if (from_jid != NULL) {
+ free(from_jid);
+ }
+ }
+
+ /* XEP-0261: Jingle In-Band Bytestreams Transport Method */
+ xmlNodePtr ibb_open = rexmpp_xml_find_child(elem, "http://jabber.org/protocol/ibb", "open");
+ if (ibb_open != NULL) {
+ handled = 1;
+ /* no-op, though could check sid here. */
+ rexmpp_iq_reply(s, elem, "result", NULL);
+ }
+ xmlNodePtr ibb_close = rexmpp_xml_find_child(elem, "http://jabber.org/protocol/ibb", "close");
+ if (ibb_close != NULL) {
+ handled = 1;
+ rexmpp_iq_reply(s, elem, "result", NULL);
+ char *sid = xmlGetProp(ibb_close, "sid");
+
+ if (sid != NULL) {
+ rexmpp_jingle_session_t *session = rexmpp_jingle_session_by_ibb_sid(s, sid);
+ if (session != NULL) {
+ rexmpp_jingle_session_terminate
+ (s, session->sid,
+ rexmpp_xml_new_node("success", "urn:xmpp:jingle:1"), NULL);
+ }
+ free(sid);
+ }
+ }
+ xmlNodePtr ibb_data = rexmpp_xml_find_child(elem, "http://jabber.org/protocol/ibb", "data");
+ if (ibb_data != NULL) {
+ handled = 1;
+ char *sid = xmlGetProp(ibb_data, "sid");
+ if (sid != NULL) {
+ rexmpp_jingle_session_t *session = rexmpp_jingle_session_by_ibb_sid(s, sid);
+ if (session != NULL && session->f != NULL) {
+ char *data = NULL, *data_base64 = xmlNodeGetContent(ibb_data);
+ if (data_base64 != NULL) {
+ size_t data_len = 0;
+ int sasl_err = gsasl_base64_from(data_base64, strlen(data_base64),
+ &data, &data_len);
+ free(data_base64);
+ if (sasl_err != GSASL_OK) {
+ rexmpp_log(s, LOG_ERR, "Base-64 decoding failure: %s",
+ gsasl_strerror(sasl_err));
+ } else {
+ size_t written = fwrite(data, 1, data_len, session->f);
+ if (written != data_len) {
+ rexmpp_log(s, LOG_ERR, "Wrote %d bytes, expected %d", written, data_len);
+ /* todo: maybe introduce buffering, or make it an error */
+ }
+ }
+ }
+ }
+ free(sid);
+ }
+ /* todo: report errors */
+ rexmpp_iq_reply(s, elem, "result", NULL);
+ }
+ return handled;
+}