From 46f60d7d807efaf173c13728975bac44228490e5 Mon Sep 17 00:00:00 2001 From: defanor Date: Sat, 25 Sep 2021 20:36:21 +0300 Subject: Implement XEP-0363: HTTP File Upload --- README | 4 +- configure.ac | 8 ++ src/Makefile.am | 9 ++- src/rexmpp.c | 68 +++++++++++++++- src/rexmpp.h | 4 + src/rexmpp_console.c | 18 +++++ src/rexmpp_http_upload.c | 205 +++++++++++++++++++++++++++++++++++++++++++++++ src/rexmpp_http_upload.h | 52 ++++++++++++ 8 files changed, 360 insertions(+), 8 deletions(-) create mode 100644 src/rexmpp_http_upload.c create mode 100644 src/rexmpp_http_upload.h diff --git a/README b/README index 418e5ac..d6b9a81 100644 --- a/README +++ b/README @@ -15,7 +15,7 @@ of implementing additional XEPs on top of it, and should try to make it easy to implement a decent client application using it. Current dependencies: libxml2, gsasl, nettle. Optionally libunbound or -c-ares, gnutls with gnutls-dane or openssl, icu-i18n, gpgme. +c-ares, gnutls with gnutls-dane or openssl, icu-i18n, gpgme, curl. A rough roadmap: @@ -69,12 +69,12 @@ A rough roadmap: [+] XEP-0172 v1.1: User Nickname [+] 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-0260: Jingle SOCKS5 Bytestreams Transport Method? [ ] XEP-0391: Jingle Encrypted Transports? -[ ] XEP-0363: HTTP File Upload? [ ] XEP-0184: Message Delivery Receipts? [ ] OTR/OMEMO/MLS encryption? [ ] A/V calls? diff --git a/configure.ac b/configure.ac index e318940..ca97aae 100644 --- a/configure.ac +++ b/configure.ac @@ -85,6 +85,14 @@ AS_IF([test "x$with_icu" != "xno"], [PKG_CHECK_MODULES([ICU_I18N], [icu-i18n], AC_DEFINE([HAVE_ICU], [1], [icu-i18n is available]))]) +# curl, optional + +AC_ARG_WITH([curl], + AS_HELP_STRING([--without-curl], [don't use curl for HTTP file upload])) +AS_IF([test "x$with_curl" != "xno"], + [PKG_CHECK_MODULES([CURL], [libcurl], + AC_DEFINE([HAVE_CURL], [1], [curl is available]))]) + # Checks for header files. AC_CHECK_HEADERS([arpa/inet.h netdb.h netinet/in.h sys/socket.h syslog.h]) diff --git a/src/Makefile.am b/src/Makefile.am index 7acc57d..391c78f 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -19,15 +19,16 @@ librexmpp_la_SOURCES = rexmpp_roster.h rexmpp_roster.c \ rexmpp_jid.h rexmpp_jid.c \ rexmpp_openpgp.h rexmpp_openpgp.c \ rexmpp_console.h rexmpp_console.c \ - rexmpp_pubsub.h rexmpp_pubsub.c + rexmpp_pubsub.h rexmpp_pubsub.c \ + rexmpp_http_upload.h rexmpp_http_upload.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_pubsub.h rexmpp_http_upload.h librexmpp_la_CFLAGS = $(AM_CFLAGS) $(LIBXML_CFLAGS) \ $(GNUTLS_CFLAGS) $(LIBDANE_CFLAGS) $(OPENSSL_CFLAGS) \ $(GSASL_CFLAGS) $(UNBOUND_CFLAGS) $(CARES_CFLAGS) $(GPGME_CFLAGS) \ - $(ICU_I18N_CFLAGS) $(NETTLE_CFLAGS) + $(ICU_I18N_CFLAGS) $(NETTLE_CFLAGS) $(CURL_CFLAGS) librexmpp_la_LIBADD = $(LIBXML_LIBS) \ $(GNUTLS_LIBS) $(LIBDANE_LIBS) $(OPENSSL_LIBS) \ $(GSASL_LIBS) $(UNBOUND_LIBS) $(CARES_LIBS) $(GPGME_LIBS) $(ICU_I18N_LIBS) \ - $(NETTLE_LIBS) + $(NETTLE_LIBS) $(CURL_LIBS) diff --git a/src/rexmpp.c b/src/rexmpp.c index 6508c9e..d1bf073 100644 --- a/src/rexmpp.c +++ b/src/rexmpp.c @@ -17,6 +17,7 @@ #include "config.h" +#include #include #include #include @@ -24,7 +25,9 @@ #ifdef HAVE_GPGME #include #endif -#include +#ifdef HAVE_CURL +#include +#endif #include "rexmpp.h" #include "rexmpp_tcp.h" @@ -34,6 +37,7 @@ #include "rexmpp_jid.h" #include "rexmpp_openpgp.h" #include "rexmpp_console.h" +#include "rexmpp_http_upload.h" struct rexmpp_iq_cacher { rexmpp_iq_callback_t cb; @@ -432,6 +436,9 @@ rexmpp_disco_find_feature (rexmpp_t *s, search->feature_var = feature_var; xmlNodePtr query = rexmpp_xml_new_node("query", "http://jabber.org/protocol/disco#info"); + if (jid == NULL) { + jid = s->initial_jid.domain; + } return rexmpp_cached_iq_new(s, "get", jid, query, rexmpp_disco_find_feature_cb, search, fresh); } @@ -620,6 +627,16 @@ rexmpp_err_t rexmpp_init (rexmpp_t *s, return REXMPP_E_PGP; } #endif +#ifdef HAVE_CURL + if (curl_global_init(CURL_GLOBAL_ALL) != 0) { + rexmpp_log(s, LOG_CRIT, "Failed to initialize curl"); + } + s->curl_multi = curl_multi_init(); + if (s->curl_multi == NULL) { + rexmpp_log(s, LOG_CRIT, "Failed to initialize curl_multi"); + /* todo: free other structures and fail */ + } +#endif return REXMPP_SUCCESS; } @@ -698,6 +715,10 @@ void rexmpp_iq_finish (rexmpp_t *s, /* Frees the things that persist through reconnects. */ void rexmpp_done (rexmpp_t *s) { rexmpp_cleanup(s); +#ifdef HAVE_CURL + curl_multi_cleanup(s->curl_multi); + curl_global_cleanup(); +#endif #ifdef HAVE_GPGME gpgme_release(s->pgp_ctx); #endif @@ -2417,6 +2438,28 @@ rexmpp_err_t rexmpp_run (rexmpp_t *s, fd_set *read_fds, fd_set *write_fds) { return REXMPP_E_OTHER; } +#ifdef HAVE_CURL + /* curl may work independently from everything else. */ + int curl_running_handles; + CURLMcode curl_code; + do { + curl_code = curl_multi_perform(s->curl_multi, &curl_running_handles); + } while (curl_code == CURLM_CALL_MULTI_PERFORM); + CURLMsg *cmsg; + int curl_queue; + do { + cmsg = curl_multi_info_read(s->curl_multi, &curl_queue); + if (cmsg != NULL && cmsg->msg == CURLMSG_DONE) { + CURL *e = cmsg->easy_handle; + struct rexmpp_http_upload_task *task; + curl_easy_getinfo(e, CURLINFO_PRIVATE, &task); + rexmpp_upload_task_finish(task); + curl_multi_remove_handle(s->curl_multi, e); + curl_easy_cleanup(e); + } + } while (cmsg != NULL); +#endif + /* Inactive: start or reconnect. */ if ((s->resolver_state == REXMPP_RESOLVER_NONE || s->resolver_state == REXMPP_RESOLVER_READY) && @@ -2608,7 +2651,7 @@ rexmpp_err_t rexmpp_run (rexmpp_t *s, fd_set *read_fds, fd_set *write_fds) { } } -int rexmpp_fds(rexmpp_t *s, fd_set *read_fds, fd_set *write_fds) { +int rexmpp_fds (rexmpp_t *s, fd_set *read_fds, fd_set *write_fds) { int conn_fd, tls_fd, max_fd = 0; if (s->resolver_state != REXMPP_RESOLVER_NONE && @@ -2616,6 +2659,14 @@ int rexmpp_fds(rexmpp_t *s, fd_set *read_fds, fd_set *write_fds) { max_fd = rexmpp_dns_fds(s, read_fds, write_fds); } +#ifdef HAVE_CURL + int curl_fd; + curl_multi_fdset(s->curl_multi, read_fds, write_fds, NULL, &curl_fd); + if (curl_fd >= max_fd) { + max_fd = curl_fd + 1; + } +#endif + if (s->tcp_state == REXMPP_TCP_CONNECTING) { conn_fd = rexmpp_tcp_conn_fds(s, &s->server_connection, read_fds, write_fds); if (conn_fd > max_fd) { @@ -2687,5 +2738,18 @@ struct timeval *rexmpp_timeout (rexmpp_t *s, } } +#ifdef HAVE_CURL + long curl_timeout; + curl_multi_timeout(s->curl_multi, &curl_timeout); + if (curl_timeout >= 0 && + (curl_timeout / 1000 < ret->tv_sec || + (curl_timeout / 1000 == ret->tv_sec && + (curl_timeout % 1000) * 1000 < ret->tv_usec))) { + tv->tv_sec = curl_timeout / 1000; + tv->tv_usec = (curl_timeout % 1000) * 1000; + ret = tv; + } +#endif + return ret; } diff --git a/src/rexmpp.h b/src/rexmpp.h index 627ec50..f3804a5 100644 --- a/src/rexmpp.h +++ b/src/rexmpp.h @@ -18,6 +18,7 @@ #ifdef HAVE_GPGME #include #endif +#include typedef struct rexmpp rexmpp_t; @@ -370,6 +371,9 @@ struct rexmpp #ifdef HAVE_GPGME gpgme_ctx_t pgp_ctx; #endif + + /* curl structures */ + CURLM *curl_multi; }; /** diff --git a/src/rexmpp_console.c b/src/rexmpp_console.c index 82086a7..1b1c603 100644 --- a/src/rexmpp_console.c +++ b/src/rexmpp_console.c @@ -14,6 +14,7 @@ #include "rexmpp.h" #include "rexmpp_openpgp.h" +#include "rexmpp_http_upload.h" #include "rexmpp_console.h" @@ -257,6 +258,16 @@ void rexmpp_console_on_run (rexmpp_t *s, rexmpp_err_t result) { } } +void rexmpp_console_on_upload (rexmpp_t *s, void *cb_data, const char *url) { + char *fpath = cb_data; + if (url == NULL) { + rexmpp_console_printf(s, "Failed to upload %s.\n", fpath); + } else { + rexmpp_console_printf(s, "Uploaded %s to <%s>.\n", fpath, url); + } + free(fpath); +} + void rexmpp_console_feed (rexmpp_t *s, char *str, ssize_t str_len) { /* todo: buffering */ (void)str_len; /* Unused for now (todo). */ @@ -288,6 +299,7 @@ void rexmpp_console_feed (rexmpp_t *s, char *str, ssize_t str_len) { "subscription request \n" "subscription approve \n" "subscription deny \n" + "http-upload \n" ; if (! strcmp(word, "help")) { @@ -539,4 +551,10 @@ void rexmpp_console_feed (rexmpp_t *s, char *str, ssize_t str_len) { rexmpp_send(s, presence); } } + + if (! strcmp(word, "http-upload")) { + char *fpath = strtok_r(NULL, " ", &words_save_ptr); + rexmpp_http_upload_path(s, NULL, fpath, NULL, + rexmpp_console_on_upload, strdup(fpath)); + } } diff --git a/src/rexmpp_http_upload.c b/src/rexmpp_http_upload.c new file mode 100644 index 0000000..17195fe --- /dev/null +++ b/src/rexmpp_http_upload.c @@ -0,0 +1,205 @@ +/** + @file rexmpp_http_upload.c + @brief XEP-0363: HTTP file upload + @author defanor + @date 2021 + @copyright MIT license. +*/ + +#include +#include +#include +#include + +#include "config.h" + +#ifdef HAVE_CURL +#include +#endif + +#include "rexmpp.h" +#include "rexmpp_http_upload.h" + + + +#ifdef HAVE_CURL +void rexmpp_upload_task_finish (struct rexmpp_http_upload_task *task) { + task->cb(task->s, task->cb_data, task->get_url); + free(task->fname); + if (task->fh != NULL) { + fclose(task->fh); + } + if (task->content_type != NULL) { + free(task->content_type); + } + if (task->get_url != NULL) { + free(task->get_url); + } + if (task->http_headers != NULL) { + curl_slist_free_all(task->http_headers); + } + free(task); +} + +void rexmpp_http_upload_slot_cb (rexmpp_t *s, + void *ptr, + xmlNodePtr request, + xmlNodePtr response, + int success) +{ + (void)request; + struct rexmpp_http_upload_task *task = ptr; + if (success) { + xmlNodePtr slot = rexmpp_xml_find_child(response, "urn:xmpp:http:upload:0", "slot"); + xmlNodePtr put = rexmpp_xml_find_child(slot, "urn:xmpp:http:upload:0", "put"); + xmlNodePtr get = rexmpp_xml_find_child(slot, "urn:xmpp:http:upload:0", "get"); + if (put != NULL && get != NULL) { + char *put_url = xmlGetProp(put, "url"); + char *get_url = xmlGetProp(get, "url"); + if (put_url != NULL && get_url != NULL) { + task->get_url = get_url; + + CURL *ce = curl_easy_init(); + curl_easy_setopt(ce, CURLOPT_PRIVATE, task); + curl_easy_setopt(ce, CURLOPT_UPLOAD, 1); + curl_easy_setopt(ce, CURLOPT_READDATA, task->fh); + curl_easy_setopt(ce, CURLOPT_URL, put_url); + + xmlNodePtr header = xmlFirstElementChild(put); + while (header) { + char *header_name = xmlGetProp(header, "name"); + if (header_name != NULL) { + char *header_str = xmlNodeGetContent(header); + if (header_str != NULL) { + size_t full_header_str_len = strlen(header_name) + 3 + strlen(header_str); + char *full_header_str = malloc(full_header_str_len); + snprintf(full_header_str, full_header_str_len, "%s: %s", + header_name, header_str); + task->http_headers = + curl_slist_append(task->http_headers, full_header_str); + free(full_header_str); + free(header_str); + } + free(header_name); + } + header = header->next; + } + curl_easy_setopt(ce, CURLOPT_HTTPHEADER, task->http_headers); + + curl_multi_add_handle(s->curl_multi, ce); + rexmpp_log(s, LOG_DEBUG, "Uploading %s to %s", task->fname, put_url); + return; + } else { + rexmpp_log(s, LOG_ERR, "Unexpected structure for a HTTP file upload slot."); + if (get_url != NULL) { + free(get_url); + } + } + if (put_url != NULL) { + free(put_url); + } + } else { + rexmpp_log(s, LOG_ERR, "Unexpected structure for a HTTP file upload slot."); + } + } else { + rexmpp_log(s, LOG_ERR, "Failed to obtain a slot for HTTP file upload."); + } + rexmpp_upload_task_finish(task); +} + +void rexmpp_http_upload_feature_cb (rexmpp_t *s, + void *ptr, + xmlNodePtr request, + xmlNodePtr response, + int success) +{ + (void)response; + struct rexmpp_http_upload_task *task = ptr; + if (! success) { + rexmpp_log(s, LOG_ERR, "No HTTP file upload service found."); + rexmpp_upload_task_finish(task); + return; + } + xmlNodePtr req = rexmpp_xml_new_node("request", "urn:xmpp:http:upload:0"); + xmlNewProp(req, "filename", task->fname); + char buf[11]; + snprintf(buf, 11, "%u", task->fsize); + xmlNewProp(req, "size", buf); + if (task->content_type) { + xmlNewProp(req, "content-type", task->content_type); + } + char *to = xmlGetProp(request, "to"); + rexmpp_iq_new(s, "get", to, req, rexmpp_http_upload_slot_cb, task); + free(to); +} + +rexmpp_err_t +rexmpp_http_upload (rexmpp_t *s, + const char *jid, + const char *fname, + size_t fsize, + FILE *fh, + const char *content_type, + http_upload_cb cb, + void *cb_data) +{ + struct rexmpp_http_upload_task *task = + malloc(sizeof(struct rexmpp_http_upload_task)); + task->fname = strdup(fname); + task->fsize = fsize; + task->fh = fh; + task->content_type = content_type ? strdup(content_type) : NULL; + task->get_url = NULL; + task->http_headers = NULL; + task->cb = cb; + task->cb_data = cb_data; + task->s = s; + return rexmpp_disco_find_feature(s, jid, "urn:xmpp:http:upload:0", + rexmpp_http_upload_feature_cb, task, 0, 20); +} +#else +rexmpp_err_t +rexmpp_http_upload (rexmpp_t *s, + const char *jid, + const char *fname, + size_t fsize, + FILE *fh, + const char *content_type, + http_upload_cb cb, + void *cb_data) +{ + (void)jid; + (void)fname; + (void)fsize; + (void)content_type; + rexmpp_log(s, LOG_ERR, "rexmpp is built without curl support"); + fclose(fh); + cb(s, cb_data, NULL); + return REXMPP_E_OTHER; +} +#endif + +rexmpp_err_t +rexmpp_http_upload_path (rexmpp_t *s, + const char *jid, + char *fpath, + const char *content_type, + http_upload_cb cb, + void *cb_data) +{ + FILE *fh = fopen(fpath, "r"); + if (fh == NULL) { + rexmpp_log(s, LOG_ERR, "Failed to open %s for reading: %s.\n", + fpath, strerror(errno)); + cb(s, cb_data, NULL); + return REXMPP_E_OTHER; + } + + char *fname = basename(fpath); + fseek(fh, 0, SEEK_END); + long fsize = ftell(fh); + fseek(fh, 0, SEEK_SET); + + return + rexmpp_http_upload(s, jid, fname, fsize, fh, content_type, cb, cb_data); +} diff --git a/src/rexmpp_http_upload.h b/src/rexmpp_http_upload.h new file mode 100644 index 0000000..898579b --- /dev/null +++ b/src/rexmpp_http_upload.h @@ -0,0 +1,52 @@ +/** + @file rexmpp_http_upload.h + @brief XEP-0363: HTTP file upload + @author defanor + @date 2021 + @copyright MIT license. +*/ + +#ifndef REXMPP_HTTP_UPLOAD_H +#define REXMPP_HTTP_UPLOAD_H + +#include "config.h" +#include "rexmpp.h" + + +typedef void (*http_upload_cb) (rexmpp_t *s, void *cb_data, const char *url); + +#ifdef HAVE_CURL +struct rexmpp_http_upload_task { + char *fname; + uint32_t fsize; + FILE *fh; + char *content_type; + char *get_url; + http_upload_cb cb; + void *cb_data; + struct curl_slist *http_headers; + rexmpp_t *s; +}; + +void rexmpp_upload_task_finish (struct rexmpp_http_upload_task *task); +#endif + +rexmpp_err_t +rexmpp_http_upload (rexmpp_t *s, + const char *jid, + const char *fname, + size_t fsize, + FILE *fh, + const char *content_type, + http_upload_cb cb, + void *cb_data); + +rexmpp_err_t +rexmpp_http_upload_path (rexmpp_t *s, + const char *jid, + char *fpath, + const char *content_type, + http_upload_cb cb, + void *cb_data); + +#endif -- cgit v1.2.3