summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordefanor <defanor@uberspace.net>2021-09-25 20:36:21 +0300
committerdefanor <defanor@uberspace.net>2021-09-25 20:36:21 +0300
commit46f60d7d807efaf173c13728975bac44228490e5 (patch)
treeed10d293bc3227e5a39ae3c3c7c1d1a2fb51c65b
parent2705c070071e54431811bf0b5d4025e04bce50c6 (diff)
Implement XEP-0363: HTTP File Upload
-rw-r--r--README4
-rw-r--r--configure.ac8
-rw-r--r--src/Makefile.am9
-rw-r--r--src/rexmpp.c68
-rw-r--r--src/rexmpp.h4
-rw-r--r--src/rexmpp_console.c18
-rw-r--r--src/rexmpp_http_upload.c205
-rw-r--r--src/rexmpp_http_upload.h52
8 files changed, 360 insertions, 8 deletions
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 <nettle/sha1.h>
#include <libxml/tree.h>
#include <libxml/xmlsave.h>
#include <gsasl.h>
@@ -24,7 +25,9 @@
#ifdef HAVE_GPGME
#include <gpgme.h>
#endif
-#include <nettle/sha1.h>
+#ifdef HAVE_CURL
+#include <curl/curl.h>
+#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 <gpgme.h>
#endif
+#include <curl/curl.h>
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 <jid>\n"
"subscription approve <jid>\n"
"subscription deny <jid>\n"
+ "http-upload <file path>\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 <defanor@uberspace.net>
+ @date 2021
+ @copyright MIT license.
+*/
+
+#include <syslog.h>
+#include <string.h>
+#include <libgen.h>
+#include <errno.h>
+
+#include "config.h"
+
+#ifdef HAVE_CURL
+#include <curl/curl.h>
+#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 <defanor@uberspace.net>
+ @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