summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordefanor <defanor@uberspace.net>2021-10-01 17:01:28 +0300
committerdefanor <defanor@uberspace.net>2021-10-01 17:03:18 +0300
commit5a1db57c9a4e3a597400f4d49432fa63cbfaefd7 (patch)
tree1c1d7068ea4fb8c803c45cc08c5af5216e7c7425
parentc094b0163ab18f9bd40d4b0e76ab3ca26372d9e4 (diff)
Add Jingle file transfer over IBB
-rw-r--r--README13
-rw-r--r--src/Makefile.am5
-rw-r--r--src/rexmpp.c176
-rw-r--r--src/rexmpp.h118
-rw-r--r--src/rexmpp_console.c19
-rw-r--r--src/rexmpp_http_upload.c2
-rw-r--r--src/rexmpp_jingle.c540
-rw-r--r--src/rexmpp_jingle.h55
8 files changed, 794 insertions, 134 deletions
diff --git a/README b/README
index d6b9a81..6f0b183 100644
--- a/README
+++ b/README
@@ -70,9 +70,10 @@ A rough roadmap:
[+] XEP-0373 v0.6: OpenPGP for XMPP
[+] XEP-0402 v1.1: PEP Native Bookmarks (autojoin conferences)
[+] XEP-0363 v1.0: HTTP File Upload (when built with curl)
-[ ] XEP-0166: Jingle
-[ ] XEP-0234: Jingle File Transfer
-[ ] XEP-0261: Jingle In-Band Bytestreams Transport Method
+[+] XEP-0166 v1.1: Jingle (only file transfers over IBB for now)
+[+] XEP-0234 v0.19: Jingle File Transfer (sending and accepting, but
+ no requests and no ranged transfers)
+[+] XEP-0261 v1.0: Jingle In-Band Bytestreams Transport Method
[ ] XEP-0260: Jingle SOCKS5 Bytestreams Transport Method?
[ ] XEP-0391: Jingle Encrypted Transports?
[ ] XEP-0184: Message Delivery Receipts?
@@ -96,12 +97,12 @@ A rough roadmap:
- Various utility functions:
[+] Display name establishment.
-[.] A console module.
-[.] XEP-0060 v1.19: Publish-Subscribe: helper functions.
+[+] A console module.
+[+] XEP-0060 v1.19: Publish-Subscribe: helper functions.
- Examples and application:
[+] Basic usage example.
[.] WeeChat plugin.
-[+] Emacs mode (and an XML-based interface).
+[+] Emacs mode (and an XML-based interface). See emacs/README.
diff --git a/src/Makefile.am b/src/Makefile.am
index 391c78f..c4b5591 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -20,10 +20,11 @@ librexmpp_la_SOURCES = rexmpp_roster.h rexmpp_roster.c \
rexmpp_openpgp.h rexmpp_openpgp.c \
rexmpp_console.h rexmpp_console.c \
rexmpp_pubsub.h rexmpp_pubsub.c \
- rexmpp_http_upload.h rexmpp_http_upload.c
+ rexmpp_http_upload.h rexmpp_http_upload.c \
+ rexmpp_jingle.h rexmpp_jingle.c
include_HEADERS = config.h rexmpp_roster.h rexmpp_tcp.h rexmpp_socks.h rexmpp.h \
rexmpp_dns.h rexmpp_tls.h rexmpp_jid.h rexmpp_openpgp.h rexmpp_console.h \
- rexmpp_pubsub.h rexmpp_http_upload.h
+ rexmpp_pubsub.h rexmpp_http_upload.h rexmpp_jingle.h
librexmpp_la_CFLAGS = $(AM_CFLAGS) $(LIBXML_CFLAGS) \
$(GNUTLS_CFLAGS) $(LIBDANE_CFLAGS) $(OPENSSL_CFLAGS) \
$(GSASL_CFLAGS) $(UNBOUND_CFLAGS) $(CARES_CFLAGS) $(GPGME_CFLAGS) \
diff --git a/src/rexmpp.c b/src/rexmpp.c
index 9c099f1..de01b2a 100644
--- a/src/rexmpp.c
+++ b/src/rexmpp.c
@@ -38,6 +38,7 @@
#include "rexmpp_openpgp.h"
#include "rexmpp_console.h"
#include "rexmpp_http_upload.h"
+#include "rexmpp_jingle.h"
struct rexmpp_iq_cacher {
rexmpp_iq_callback_t cb;
@@ -474,6 +475,17 @@ xmlNodePtr rexmpp_disco_info (rexmpp_t *s) {
prev->next = cur;
prev = cur;
}
+ if (s->enable_jingle) {
+ cur = rexmpp_xml_feature("urn:xmpp:jingle:1");
+ prev->next = cur;
+ prev = cur;
+ cur = rexmpp_xml_feature("urn:xmpp:jingle:apps:file-transfer:5");
+ prev->next = cur;
+ prev = cur;
+ cur = rexmpp_xml_feature("urn:xmpp:jingle:transports:ibb:1");
+ prev->next = cur;
+ prev = cur;
+ }
cur = rexmpp_xml_feature("urn:xmpp:ping");
prev->next = cur;
prev = cur;
@@ -527,6 +539,7 @@ rexmpp_err_t rexmpp_init (rexmpp_t *s,
#endif
s->autojoin_bookmarked_mucs = 1;
s->tls_policy = REXMPP_TLS_REQUIRE;
+ s->enable_jingle = 1;
s->client_name = PACKAGE_NAME;
s->client_type = "console";
s->client_version = PACKAGE_VERSION;
@@ -550,6 +563,7 @@ rexmpp_err_t rexmpp_init (rexmpp_t *s,
s->stream_id = NULL;
s->active_iq = NULL;
s->iq_cache = NULL;
+ s->jingle = NULL;
s->reconnect_number = 0;
s->next_reconnect_time.tv_sec = 0;
s->next_reconnect_time.tv_usec = 0;
@@ -559,6 +573,7 @@ rexmpp_err_t rexmpp_init (rexmpp_t *s,
s->send_queue_size = 1024;
s->iq_queue_size = 1024;
s->iq_cache_size = 1024;
+ s->max_jingle_sessions = 1024;
s->log_function = log_func;
s->sasl_property_cb = NULL;
s->xml_in_cb = NULL;
@@ -764,6 +779,7 @@ void rexmpp_done (rexmpp_t *s) {
xmlFreeNodeList(s->iq_cache);
s->iq_cache = NULL;
}
+ rexmpp_jingle_stop(s);
}
void rexmpp_schedule_reconnect (rexmpp_t *s) {
@@ -805,7 +821,7 @@ const char *jid_bare_to_host (const char *jid_bare) {
return NULL;
}
-xmlNodePtr rexmpp_xml_add_id (rexmpp_t *s, xmlNodePtr node) {
+char *rexmpp_gen_id (rexmpp_t *s) {
int sasl_err;
char buf_raw[18], *buf_base64 = NULL;
size_t buf_base64_len = 0;
@@ -821,8 +837,16 @@ xmlNodePtr rexmpp_xml_add_id (rexmpp_t *s, xmlNodePtr node) {
gsasl_strerror(sasl_err));
return NULL;
}
- xmlNewProp(node, "id", buf_base64);
- free(buf_base64);
+ return buf_base64;
+}
+
+xmlNodePtr rexmpp_xml_add_id (rexmpp_t *s, xmlNodePtr node) {
+ char *buf = rexmpp_gen_id(s);
+ if (buf == NULL) {
+ return NULL;
+ }
+ xmlNewProp(node, "id", buf);
+ free(buf);
return node;
}
@@ -1922,88 +1946,86 @@ rexmpp_err_t rexmpp_process_element (rexmpp_t *s, xmlNodePtr elem) {
req = req_next;
}
free(id);
- }
- /* IQ "set" requests. */
- if (strcmp(type, "set") == 0) {
- xmlNodePtr query = xmlFirstElementChild(elem);
- int from_server = 0;
- char *from = xmlGetProp(elem, "from");
- if (from == NULL) {
- from_server = 1;
- } else {
- if (strcmp(from, s->initial_jid.domain) == 0) {
+ } else if (! rexmpp_jingle_iq(s, elem)) {
+ if (strcmp(type, "set") == 0) {
+ xmlNodePtr query = xmlFirstElementChild(elem);
+ int from_server = 0;
+ char *from = xmlGetProp(elem, "from");
+ if (from == NULL) {
from_server = 1;
+ } else {
+ if (strcmp(from, s->initial_jid.domain) == 0) {
+ from_server = 1;
+ }
+ free(from);
}
- free(from);
- }
- if (from_server &&
- s->manage_roster &&
- rexmpp_xml_match(query, "jabber:iq:roster", "query")) {
- /* Roster push. */
- if (s->roster_ver != NULL) {
- free(s->roster_ver);
- }
- s->roster_ver = xmlGetProp(query, "ver");
- rexmpp_modify_roster(s, xmlFirstElementChild(query));
- /* todo: check for errors */
- rexmpp_iq_reply(s, elem, "result", NULL);
- if (s->roster_cache_file != NULL) {
- rexmpp_roster_cache_write(s);
+ if (from_server &&
+ s->manage_roster &&
+ rexmpp_xml_match(query, "jabber:iq:roster", "query")) {
+ /* Roster push. */
+ if (s->roster_ver != NULL) {
+ free(s->roster_ver);
+ }
+ s->roster_ver = xmlGetProp(query, "ver");
+ rexmpp_modify_roster(s, xmlFirstElementChild(query));
+ /* todo: check for errors */
+ rexmpp_iq_reply(s, elem, "result", NULL);
+ if (s->roster_cache_file != NULL) {
+ rexmpp_roster_cache_write(s);
+ }
+ } else {
+ /* An unknown request. */
+ rexmpp_iq_reply(s, elem, "error",
+ rexmpp_xml_error("cancel", "service-unavailable"));
}
- } else {
- /* An unknown request. */
- rexmpp_iq_reply(s, elem, "error",
- rexmpp_xml_error("cancel", "service-unavailable"));
- }
- }
- /* IQ "get" requests. */
- if (strcmp(type, "get") == 0) {
- xmlNodePtr query = xmlFirstElementChild(elem);
- if (rexmpp_xml_match(query, "http://jabber.org/protocol/disco#info", "query")) {
- char *node = xmlGetProp(query, "node");
- char *caps_hash = rexmpp_capabilities_hash(s, rexmpp_disco_info(s));
- if (node == NULL ||
- (caps_hash != NULL &&
- s->disco_node != NULL &&
- strlen(node) == strlen(s->disco_node) + 1 + strlen(caps_hash) &&
- strncmp(node, s->disco_node, strlen(s->disco_node)) == 0 &&
- node[strlen(s->disco_node)] == '#' &&
- strcmp(node + strlen(s->disco_node) + 1, caps_hash) == 0)) {
- xmlNodePtr result = xmlNewNode(NULL, "query");
- xmlNewNs(result, "http://jabber.org/protocol/disco#info", NULL);
+ } else if (strcmp(type, "get") == 0) {
+ xmlNodePtr query = xmlFirstElementChild(elem);
+ if (rexmpp_xml_match(query, "http://jabber.org/protocol/disco#info", "query")) {
+ char *node = xmlGetProp(query, "node");
+ char *caps_hash = rexmpp_capabilities_hash(s, rexmpp_disco_info(s));
+ if (node == NULL ||
+ (caps_hash != NULL &&
+ s->disco_node != NULL &&
+ strlen(node) == strlen(s->disco_node) + 1 + strlen(caps_hash) &&
+ strncmp(node, s->disco_node, strlen(s->disco_node)) == 0 &&
+ node[strlen(s->disco_node)] == '#' &&
+ strcmp(node + strlen(s->disco_node) + 1, caps_hash) == 0)) {
+ xmlNodePtr result = xmlNewNode(NULL, "query");
+ xmlNewNs(result, "http://jabber.org/protocol/disco#info", NULL);
+ if (node != NULL) {
+ xmlNewProp(result, "node", node);
+ }
+ xmlAddChild(result, xmlCopyNodeList(rexmpp_disco_info(s)));
+ rexmpp_iq_reply(s, elem, "result", result);
+ } else {
+ rexmpp_log(s, LOG_WARNING,
+ "Service discovery request for an unknown node: %s", node);
+ rexmpp_iq_reply(s, elem, "error",
+ rexmpp_xml_error("cancel", "item-not-found"));
+ }
+ if (caps_hash != NULL) {
+ free(caps_hash);
+ }
if (node != NULL) {
- xmlNewProp(result, "node", node);
+ free(node);
}
- xmlAddChild(result, xmlCopyNodeList(rexmpp_disco_info(s)));
- rexmpp_iq_reply(s, elem, "result", result);
+ } else if (rexmpp_xml_match(query, "urn:xmpp:ping", "ping")) {
+ rexmpp_iq_reply(s, elem, "result", NULL);
+ } else if (rexmpp_xml_match(query, "jabber:iq:version", "query")) {
+ xmlNodePtr reply = xmlNewNode(NULL, "query");
+ xmlNewNs(reply, "jabber:iq:version", NULL);
+ xmlNodePtr name = xmlNewNode(NULL, "name");
+ xmlNodeAddContent(name, s->client_name);
+ xmlAddChild(reply, name);
+ xmlNodePtr version = xmlNewNode(NULL, "version");
+ xmlNodeAddContent(version, s->client_version);
+ xmlAddChild(reply, version);
+ rexmpp_iq_reply(s, elem, "result", reply);
} else {
- rexmpp_log(s, LOG_WARNING,
- "Service discovery request for an unknown node: %s", node);
+ /* An unknown request. */
rexmpp_iq_reply(s, elem, "error",
- rexmpp_xml_error("cancel", "item-not-found"));
- }
- if (caps_hash != NULL) {
- free(caps_hash);
+ rexmpp_xml_error("cancel", "service-unavailable"));
}
- if (node != NULL) {
- free(node);
- }
- } else if (rexmpp_xml_match(query, "urn:xmpp:ping", "ping")) {
- rexmpp_iq_reply(s, elem, "result", NULL);
- } else if (rexmpp_xml_match(query, "jabber:iq:version", "query")) {
- xmlNodePtr reply = xmlNewNode(NULL, "query");
- xmlNewNs(reply, "jabber:iq:version", NULL);
- xmlNodePtr name = xmlNewNode(NULL, "name");
- xmlNodeAddContent(name, s->client_name);
- xmlAddChild(reply, name);
- xmlNodePtr version = xmlNewNode(NULL, "version");
- xmlNodeAddContent(version, s->client_version);
- xmlAddChild(reply, version);
- rexmpp_iq_reply(s, elem, "result", reply);
- } else {
- /* An unknown request. */
- rexmpp_iq_reply(s, elem, "error",
- rexmpp_xml_error("cancel", "service-unavailable"));
}
}
free(type);
diff --git a/src/rexmpp.h b/src/rexmpp.h
index f3804a5..99ebb90 100644
--- a/src/rexmpp.h
+++ b/src/rexmpp.h
@@ -22,11 +22,61 @@
typedef struct rexmpp rexmpp_t;
+/** Error codes. */
+enum rexmpp_err {
+ /** An operation is finished. */
+ REXMPP_SUCCESS,
+ /** An operation is in progress. */
+ REXMPP_E_AGAIN,
+ /** A message can't be queued for sending, because the queue is
+ full. */
+ REXMPP_E_SEND_QUEUE_FULL,
+ /** The library can't take responsibility for message delivery (and
+ doesn't try to send it), because XEP-0198 stanza queue is
+ full. */
+ REXMPP_E_STANZA_QUEUE_FULL,
+ /** An operation (reading or sending) was cancelled by a user. */
+ REXMPP_E_CANCELLED,
+ /** An attempt to send while send buffer is empty. */
+ REXMPP_E_SEND_BUFFER_EMPTY,
+ /** An attempt to start sending while send buffer is not empty. */
+ REXMPP_E_SEND_BUFFER_NOT_EMPTY,
+ /** SASL-related error. */
+ REXMPP_E_SASL,
+ /** OpenPGP-related error. */
+ REXMPP_E_PGP,
+ /** TLS-related error. */
+ REXMPP_E_TLS,
+ /** TCP-related error. */
+ REXMPP_E_TCP,
+ /** DNS-related error. */
+ REXMPP_E_DNS,
+ /** XML-related error. */
+ REXMPP_E_XML,
+ /** JID-related error. */
+ REXMPP_E_JID,
+ /** Failure to allocate memory. */
+ REXMPP_E_MALLOC,
+ /** Roster-related error. */
+ REXMPP_E_ROSTER,
+ /** A roster item is not found. */
+ REXMPP_E_ROSTER_ITEM_NOT_FOUND,
+ /** An erroneous parameter is supplied. */
+ REXMPP_E_PARAM,
+ /** A stream error. */
+ REXMPP_E_STREAM,
+ /** An unspecified error. */
+ REXMPP_E_OTHER
+};
+
+typedef enum rexmpp_err rexmpp_err_t;
+
#include "rexmpp_tcp.h"
#include "rexmpp_socks.h"
#include "rexmpp_dns.h"
#include "rexmpp_tls.h"
#include "rexmpp_jid.h"
+#include "rexmpp_jingle.h"
/**
@brief An info/query callback function type.
@@ -168,54 +218,6 @@ enum carbons_st {
REXMPP_CARBONS_ACTIVE
};
-/** Error codes. */
-enum rexmpp_err {
- /** An operation is finished. */
- REXMPP_SUCCESS,
- /** An operation is in progress. */
- REXMPP_E_AGAIN,
- /** A message can't be queued for sending, because the queue is
- full. */
- REXMPP_E_SEND_QUEUE_FULL,
- /** The library can't take responsibility for message delivery (and
- doesn't try to send it), because XEP-0198 stanza queue is
- full. */
- REXMPP_E_STANZA_QUEUE_FULL,
- /** An operation (reading or sending) was cancelled by a user. */
- REXMPP_E_CANCELLED,
- /** An attempt to send while send buffer is empty. */
- REXMPP_E_SEND_BUFFER_EMPTY,
- /** An attempt to start sending while send buffer is not empty. */
- REXMPP_E_SEND_BUFFER_NOT_EMPTY,
- /** SASL-related error. */
- REXMPP_E_SASL,
- /** OpenPGP-related error. */
- REXMPP_E_PGP,
- /** TLS-related error. */
- REXMPP_E_TLS,
- /** TCP-related error. */
- REXMPP_E_TCP,
- /** DNS-related error. */
- REXMPP_E_DNS,
- /** XML-related error. */
- REXMPP_E_XML,
- /** JID-related error. */
- REXMPP_E_JID,
- /** Failure to allocate memory. */
- REXMPP_E_MALLOC,
- /** Roster-related error. */
- REXMPP_E_ROSTER,
- /** A roster item is not found. */
- REXMPP_E_ROSTER_ITEM_NOT_FOUND,
- /** An erroneous parameter is supplied. */
- REXMPP_E_PARAM,
- /** A stream error. */
- REXMPP_E_STREAM,
- /** An unspecified error. */
- REXMPP_E_OTHER
-};
-typedef enum rexmpp_err rexmpp_err_t;
-
/** @brief TLS policy */
enum tls_pol {
REXMPP_TLS_REQUIRE,
@@ -269,6 +271,7 @@ struct rexmpp
int retrieve_openpgp_keys; /* XEP-0373 */
int autojoin_bookmarked_mucs; /* XEP-0402 */
enum tls_pol tls_policy;
+ int enable_jingle;
const char *client_name; /* XEP-0030, XEP-0092 */
const char *client_type; /* XEP-0030 */
const char *client_version; /* XEP-0092 */
@@ -278,6 +281,7 @@ struct rexmpp
uint32_t send_queue_size;
uint32_t iq_queue_size;
uint32_t iq_cache_size;
+ uint32_t max_jingle_sessions;
/* Callbacks. */
log_function_t log_function;
@@ -304,6 +308,9 @@ struct rexmpp
/* Cached IQ requests and responses. */
xmlNodePtr iq_cache;
+ /* Active Jingle sessions. */
+ rexmpp_jingle_session_t *jingle;
+
/* Connection and stream management. */
unsigned int reconnect_number;
time_t reconnect_seconds;
@@ -452,6 +459,14 @@ rexmpp_err_t rexmpp_cached_iq_new (rexmpp_t *s,
int fresh);
/**
+ @brief Reply to an IQ.
+*/
+void rexmpp_iq_reply (rexmpp_t *s,
+ xmlNodePtr req,
+ const char *type,
+ xmlNodePtr payload);
+
+/**
@brief Determines the maximum time to wait before the next
::rexmpp_run call.
@param[in] s ::rexmpp
@@ -478,6 +493,11 @@ struct timeval *rexmpp_timeout (rexmpp_t *s,
int rexmpp_fds (rexmpp_t *s, fd_set *read_fds, fd_set *write_fds);
/**
+ @brief Compose an 'error' element.
+*/
+xmlNodePtr rexmpp_xml_error (const char *type, const char *condition);
+
+/**
@brief A helper function for XML parsing.
@param[in] str A string to parse.
@param[in] str_len String length.
@@ -553,6 +573,8 @@ xmlNodePtr rexmpp_xml_find_child (xmlNodePtr node,
xmlNodePtr rexmpp_xml_new_node (const char *name, const char *namespace);
+char *rexmpp_gen_id (rexmpp_t *s);
+
/**
@brief Finds a PEP event.
@param[in] s ::rexmpp
diff --git a/src/rexmpp_console.c b/src/rexmpp_console.c
index 1b1c603..397e603 100644
--- a/src/rexmpp_console.c
+++ b/src/rexmpp_console.c
@@ -15,6 +15,7 @@
#include "rexmpp.h"
#include "rexmpp_openpgp.h"
#include "rexmpp_http_upload.h"
+#include "rexmpp_jingle.h"
#include "rexmpp_console.h"
@@ -300,6 +301,8 @@ void rexmpp_console_feed (rexmpp_t *s, char *str, ssize_t str_len) {
"subscription approve <jid>\n"
"subscription deny <jid>\n"
"http-upload <file path>\n"
+ "jingle accept-file <sid> <file path>\n"
+ "jingle send-file <jid> <file path>\n"
;
if (! strcmp(word, "help")) {
@@ -557,4 +560,20 @@ void rexmpp_console_feed (rexmpp_t *s, char *str, ssize_t str_len) {
rexmpp_http_upload_path(s, NULL, fpath, NULL,
rexmpp_console_on_upload, strdup(fpath));
}
+
+ if (! strcmp(word, "jingle")) {
+ word = strtok_r(NULL, " ", &words_save_ptr);
+ if (word == NULL) {
+ return;
+ }
+ if (! strcmp(word, "accept-file")) {
+ char *sid = strtok_r(NULL, " ", &words_save_ptr);
+ char *fpath = strtok_r(NULL, " ", &words_save_ptr);
+ rexmpp_jingle_accept_file_by_id(s, sid, fpath);
+ } else if (! strcmp(word, "send-file")) {
+ char *jid = strtok_r(NULL, " ", &words_save_ptr);
+ char *fpath = strtok_r(NULL, " ", &words_save_ptr);
+ rexmpp_jingle_send_file(s, jid, fpath);
+ }
+ }
}
diff --git a/src/rexmpp_http_upload.c b/src/rexmpp_http_upload.c
index 17195fe..62d371b 100644
--- a/src/rexmpp_http_upload.c
+++ b/src/rexmpp_http_upload.c
@@ -187,7 +187,7 @@ rexmpp_http_upload_path (rexmpp_t *s,
http_upload_cb cb,
void *cb_data)
{
- FILE *fh = fopen(fpath, "r");
+ FILE *fh = fopen(fpath, "rb");
if (fh == NULL) {
rexmpp_log(s, LOG_ERR, "Failed to open %s for reading: %s.\n",
fpath, strerror(errno));
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;
+}
diff --git a/src/rexmpp_jingle.h b/src/rexmpp_jingle.h
new file mode 100644
index 0000000..c8a2822
--- /dev/null
+++ b/src/rexmpp_jingle.h
@@ -0,0 +1,55 @@
+/**
+ @file rexmpp_jingle.h
+ @brief Jingle routines
+ @author defanor <defanor@uberspace.net>
+ @date 2021
+ @copyright MIT license.
+
+*/
+
+
+#ifndef REXMPP_JINGLE_H
+#define REXMPP_JINGLE_H
+
+#include "rexmpp.h"
+
+/** @brief Processes incoming Jingle IQs. */
+int rexmpp_jingle_iq (rexmpp_t *s, xmlNodePtr elem);
+
+/** @brief Destroys Jingle sessions. */
+void rexmpp_jingle_stop (rexmpp_t *s);
+
+/** @brief Accepts a file, given a sid and a path to save it to. */
+rexmpp_err_t
+rexmpp_jingle_accept_file_by_id (rexmpp_t *s,
+ const char *sid,
+ const char *path);
+
+/** @brief Sends a file to a given full JID. */
+rexmpp_err_t
+rexmpp_jingle_send_file (rexmpp_t *s,
+ const char *jid,
+ char *path);
+
+/** @brief Terminates a Jingle session. */
+rexmpp_err_t
+rexmpp_jingle_session_terminate (rexmpp_t *s,
+ const char *sid,
+ xmlNodePtr reason_node,
+ const char *reason_text);
+
+typedef struct rexmpp_jingle_session rexmpp_jingle_session_t;
+
+struct rexmpp_jingle_session {
+ char *jid;
+ char *sid;
+ char *ibb_sid;
+ uint16_t ibb_seq;
+ /* The most recent <jingle/> elmment in negotiation. */
+ xmlNodePtr negotiation;
+ FILE *f;
+ rexmpp_jingle_session_t *next;
+};
+
+
+#endif