From 5a1db57c9a4e3a597400f4d49432fa63cbfaefd7 Mon Sep 17 00:00:00 2001 From: defanor Date: Fri, 1 Oct 2021 17:01:28 +0300 Subject: Add Jingle file transfer over IBB --- README | 13 +- src/Makefile.am | 5 +- src/rexmpp.c | 176 ++++++++------- src/rexmpp.h | 118 ++++++----- src/rexmpp_console.c | 19 ++ src/rexmpp_http_upload.c | 2 +- src/rexmpp_jingle.c | 540 +++++++++++++++++++++++++++++++++++++++++++++++ src/rexmpp_jingle.h | 55 +++++ 8 files changed, 794 insertions(+), 134 deletions(-) create mode 100644 src/rexmpp_jingle.c create mode 100644 src/rexmpp_jingle.h 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; @@ -451,6 +458,14 @@ rexmpp_err_t rexmpp_cached_iq_new (rexmpp_t *s, void *cb_data, 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. @@ -477,6 +492,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. @@ -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 \n" "subscription deny \n" "http-upload \n" + "jingle accept-file \n" + "jingle send-file \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 + @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 +#include +#include +#include +#include +#include + +#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 + @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 elmment in negotiation. */ + xmlNodePtr negotiation; + FILE *f; + rexmpp_jingle_session_t *next; +}; + + +#endif -- cgit v1.2.3