summary refs log tree commit diff
path: root/src/nm-cloud-setup/nm-http-client.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/nm-cloud-setup/nm-http-client.c')
-rw-r--r--src/nm-cloud-setup/nm-http-client.c801
1 files changed, 801 insertions, 0 deletions
diff --git a/src/nm-cloud-setup/nm-http-client.c b/src/nm-cloud-setup/nm-http-client.c
new file mode 100644
index 00000000..618ab1be
--- /dev/null
+++ b/src/nm-cloud-setup/nm-http-client.c
@@ -0,0 +1,801 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include "nm-http-client.h"
+
+#include <curl/curl.h>
+
+#include "nm-cloud-setup-utils.h"
+#include "libnm-glib-aux/nm-str-buf.h"
+
+#define NM_CURL_DEBUG 0
+
+/*****************************************************************************/
+
+typedef struct {
+    GMainContext *context;
+    CURLM *       mhandle;
+    GSource *     mhandle_source_timeout;
+    GHashTable *  source_sockets_hashtable;
+} NMHttpClientPrivate;
+
+struct _NMHttpClient {
+    GObject             parent;
+    NMHttpClientPrivate _priv;
+};
+
+struct _NMHttpClientClass {
+    GObjectClass parent;
+};
+
+G_DEFINE_TYPE(NMHttpClient, nm_http_client, G_TYPE_OBJECT);
+
+#define NM_HTTP_CLIENT_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMHttpClient, NM_IS_HTTP_CLIENT)
+
+/*****************************************************************************/
+
+#define _NMLOG2(level, edata, ...)                               \
+    G_STMT_START                                                 \
+    {                                                            \
+        EHandleData *_edata = (edata);                           \
+                                                                 \
+        _NMLOG(level,                                            \
+               "http-request[" NM_HASH_OBFUSCATE_PTR_FMT         \
+               ", \"%s\"]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \
+               NM_HASH_OBFUSCATE_PTR(_edata),                    \
+               (_edata)->url _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
+    }                                                            \
+    G_STMT_END
+
+/*****************************************************************************/
+
+G_LOCK_DEFINE_STATIC(_my_curl_initalized_lock);
+static bool _my_curl_initialized = FALSE;
+
+__attribute__((destructor)) static void
+_my_curl_global_cleanup(void)
+{
+    G_LOCK(_my_curl_initalized_lock);
+    if (_my_curl_initialized) {
+        _my_curl_initialized = FALSE;
+        curl_global_cleanup();
+    }
+    G_UNLOCK(_my_curl_initalized_lock);
+}
+
+static void
+nm_http_client_curl_global_init(void)
+{
+    G_LOCK(_my_curl_initalized_lock);
+    if (!_my_curl_initialized) {
+        _my_curl_initialized = TRUE;
+        if (curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK) {
+            /* Even if this fails, we are partly initialized. WTF. */
+            _LOGE("curl: curl_global_init() failed!");
+        }
+    }
+    G_UNLOCK(_my_curl_initalized_lock);
+}
+
+/*****************************************************************************/
+
+GMainContext *
+nm_http_client_get_main_context(NMHttpClient *self)
+{
+    g_return_val_if_fail(NM_IS_HTTP_CLIENT(self), NULL);
+
+    return NM_HTTP_CLIENT_GET_PRIVATE(self)->context;
+}
+
+/*****************************************************************************/
+
+static GSource *
+_source_attach(NMHttpClient *self, GSource *source)
+{
+    return nm_g_source_attach(source, NM_HTTP_CLIENT_GET_PRIVATE(self)->context);
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    long    response_code;
+    GBytes *response_data;
+} GetResult;
+
+static void
+_get_result_free(gpointer data)
+{
+    GetResult *get_result = data;
+
+    g_bytes_unref(get_result->response_data);
+    nm_g_slice_free(get_result);
+}
+
+typedef struct {
+    GTask *            task;
+    GSource *          timeout_source;
+    CURLcode           ehandle_result;
+    CURL *             ehandle;
+    char *             url;
+    NMStrBuf           recv_data;
+    struct curl_slist *headers;
+    gssize             max_data;
+    gulong             cancellable_id;
+} EHandleData;
+
+static void
+_ehandle_free_ehandle(EHandleData *edata)
+{
+    if (edata->ehandle) {
+        NMHttpClient *       self = g_task_get_source_object(edata->task);
+        NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE(self);
+
+        curl_multi_remove_handle(priv->mhandle, edata->ehandle);
+        curl_easy_cleanup(g_steal_pointer(&edata->ehandle));
+    }
+}
+
+static void
+_ehandle_free(EHandleData *edata)
+{
+    nm_assert(!edata->ehandle);
+    nm_assert(!edata->timeout_source);
+
+    g_object_unref(edata->task);
+
+    nm_str_buf_destroy(&edata->recv_data);
+    if (edata->headers)
+        curl_slist_free_all(edata->headers);
+    g_free(edata->url);
+    nm_g_slice_free(edata);
+}
+
+static void
+_ehandle_complete(EHandleData *edata, GError *error_take)
+{
+    GetResult *   get_result;
+    gs_free char *str_tmp_1     = NULL;
+    long          response_code = -1;
+
+    nm_clear_pointer(&edata->timeout_source, nm_g_source_destroy_and_unref);
+
+    nm_clear_g_cancellable_disconnect(g_task_get_cancellable(edata->task), &edata->cancellable_id);
+
+    if (error_take) {
+        if (nm_utils_error_is_cancelled(error_take))
+            _LOG2T(edata, "cancelled");
+        else
+            _LOG2D(edata, "failed with %s", error_take->message);
+    } else if (edata->ehandle_result != CURLE_OK) {
+        _LOG2D(edata, "failed with curl error \"%s\"", curl_easy_strerror(edata->ehandle_result));
+        nm_utils_error_set(&error_take,
+                           NM_UTILS_ERROR_UNKNOWN,
+                           "failed with curl error \"%s\"",
+                           curl_easy_strerror(edata->ehandle_result));
+    }
+
+    if (error_take) {
+        _ehandle_free_ehandle(edata);
+        g_task_return_error(edata->task, error_take);
+        _ehandle_free(edata);
+        return;
+    }
+
+    if (curl_easy_getinfo(edata->ehandle, CURLINFO_RESPONSE_CODE, &response_code) != CURLE_OK)
+        _LOG2E(edata, "failed to get response code from curl easy handle");
+
+    _LOG2D(edata,
+           "success getting %" G_GSIZE_FORMAT " bytes (response code %ld)",
+           edata->recv_data.len,
+           response_code);
+
+    _LOG2T(edata,
+           "received %" G_GSIZE_FORMAT " bytes: [[%s]]",
+           edata->recv_data.len,
+           nm_utils_buf_utf8safe_escape(nm_str_buf_get_str(&edata->recv_data),
+                                        edata->recv_data.len,
+                                        NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL,
+                                        &str_tmp_1));
+
+    _ehandle_free_ehandle(edata);
+
+    get_result  = g_slice_new(GetResult);
+    *get_result = (GetResult){
+        .response_code = response_code,
+        /* This ensures that response_data is always NUL terminated. This is an important guarantee
+         * that NMHttpClient makes. */
+        .response_data = nm_str_buf_finalize_to_gbytes(&edata->recv_data),
+    };
+
+    g_task_return_pointer(edata->task, get_result, _get_result_free);
+
+    _ehandle_free(edata);
+}
+
+/*****************************************************************************/
+
+static size_t
+_get_writefunction_cb(char *ptr, size_t size, size_t nmemb, void *user_data)
+{
+    EHandleData *edata = user_data;
+    gsize        nconsume;
+
+    /* size should always be 1, but still. Multiply them to be sure. */
+    nmemb *= size;
+
+    if (edata->max_data >= 0) {
+        nm_assert(edata->recv_data.len <= edata->max_data);
+        nconsume = (((gsize) edata->max_data) - edata->recv_data.len);
+        if (nconsume > nmemb)
+            nconsume = nmemb;
+    } else
+        nconsume = nmemb;
+
+    nm_str_buf_append_len(&edata->recv_data, ptr, nconsume);
+    return nconsume;
+}
+
+static gboolean
+_get_timeout_cb(gpointer user_data)
+{
+    _ehandle_complete(
+        user_data,
+        g_error_new_literal(NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "HTTP request timed out"));
+    return G_SOURCE_REMOVE;
+}
+
+static void
+_get_cancelled_cb(GObject *object, gpointer user_data)
+{
+    EHandleData *edata = user_data;
+    GError *     error = NULL;
+
+    nm_clear_g_signal_handler(g_task_get_cancellable(edata->task), &edata->cancellable_id);
+    nm_utils_error_set_cancelled(&error, FALSE, NULL);
+    _ehandle_complete(edata, error);
+}
+
+void
+nm_http_client_get(NMHttpClient *      self,
+                   const char *        url,
+                   int                 timeout_msec,
+                   gssize              max_data,
+                   const char *const * http_headers,
+                   GCancellable *      cancellable,
+                   GAsyncReadyCallback callback,
+                   gpointer            user_data)
+{
+    NMHttpClientPrivate *priv;
+    EHandleData *        edata;
+    guint                i;
+
+    g_return_if_fail(NM_IS_HTTP_CLIENT(self));
+    g_return_if_fail(url);
+    g_return_if_fail(!cancellable || G_IS_CANCELLABLE(cancellable));
+    g_return_if_fail(timeout_msec >= 0);
+    g_return_if_fail(max_data >= -1);
+
+    priv = NM_HTTP_CLIENT_GET_PRIVATE(self);
+
+    edata  = g_slice_new(EHandleData);
+    *edata = (EHandleData){
+        .task      = nm_g_task_new(self, cancellable, nm_http_client_get, callback, user_data),
+        .recv_data = NM_STR_BUF_INIT(0, FALSE),
+        .max_data  = max_data,
+        .url       = g_strdup(url),
+        .headers   = NULL,
+    };
+
+    nmcs_wait_for_objects_register(edata->task);
+
+    _LOG2D(edata, "start get ...");
+
+    edata->ehandle = curl_easy_init();
+    if (!edata->ehandle) {
+        _ehandle_complete(edata,
+                          g_error_new_literal(NM_UTILS_ERROR,
+                                              NM_UTILS_ERROR_UNKNOWN,
+                                              "HTTP request failed to create curl handle"));
+        return;
+    }
+
+    curl_easy_setopt(edata->ehandle, CURLOPT_URL, url);
+
+    curl_easy_setopt(edata->ehandle, CURLOPT_WRITEFUNCTION, _get_writefunction_cb);
+    curl_easy_setopt(edata->ehandle, CURLOPT_WRITEDATA, edata);
+    curl_easy_setopt(edata->ehandle, CURLOPT_PRIVATE, edata);
+
+    if (http_headers) {
+        for (i = 0; http_headers[i]; ++i) {
+            struct curl_slist *tmp;
+
+            tmp = curl_slist_append(edata->headers, http_headers[i]);
+            if (!tmp) {
+                curl_slist_free_all(tmp);
+                _LOGE("curl: curl_slist_append() failed adding %s", http_headers[i]);
+                continue;
+            }
+            edata->headers = tmp;
+        }
+
+        curl_easy_setopt(edata->ehandle, CURLOPT_HTTPHEADER, edata->headers);
+    }
+
+    if (timeout_msec > 0) {
+        edata->timeout_source = _source_attach(self,
+                                               nm_g_timeout_source_new(timeout_msec,
+                                                                       G_PRIORITY_DEFAULT,
+                                                                       _get_timeout_cb,
+                                                                       edata,
+                                                                       NULL));
+    }
+
+    curl_multi_add_handle(priv->mhandle, edata->ehandle);
+
+    if (cancellable) {
+        gulong signal_id;
+
+        signal_id = g_cancellable_connect(cancellable, G_CALLBACK(_get_cancelled_cb), edata, NULL);
+        if (signal_id == 0) {
+            /* the request is already cancelled. Return. */
+            return;
+        }
+        edata->cancellable_id = signal_id;
+    }
+}
+
+/**
+ * nm_http_client_get_finish:
+ * @self: the #NMHttpClient instance
+ * @result: the #GAsyncResult which to complete.
+ * @out_response_code: (allow-none) (out): the HTTP response code or -1 on other error.
+ * @out_response_data: (allow-none) (transfer full): the HTTP response data, if any.
+ *   The GBytes buffer is guaranteed to have a trailing NUL character *after* the
+ *   returned buffer size. That means, you can always trust that the buffer is NUL terminated
+ *   and that there is one additional hidden byte after the data.
+ *   Also, the returned buffer is allocated just for you. While GBytes is immutable, you are
+ *   allowed to modify the buffer as it's not used by anybody else.
+ * @error: the error
+ *
+ * Returns: %TRUE on success or %FALSE with an error code.
+ */
+gboolean
+nm_http_client_get_finish(NMHttpClient *self,
+                          GAsyncResult *result,
+                          long *        out_response_code,
+                          GBytes **     out_response_data,
+                          GError **     error)
+{
+    GetResult *get_result;
+
+    g_return_val_if_fail(NM_IS_HTTP_CLIENT(self), FALSE);
+    g_return_val_if_fail(nm_g_task_is_valid(result, self, nm_http_client_get), FALSE);
+
+    get_result = g_task_propagate_pointer(G_TASK(result), error);
+
+    nm_assert(!error || (!!get_result) == (!*error));
+
+    if (!get_result) {
+        NM_SET_OUT(out_response_code, -1);
+        NM_SET_OUT(out_response_data, NULL);
+        return FALSE;
+    }
+
+    NM_SET_OUT(out_response_code, get_result->response_code);
+
+    /* response_data is binary, but is also guaranteed to be NUL terminated! */
+    NM_SET_OUT(out_response_data, g_steal_pointer(&get_result->response_data));
+
+    _get_result_free(get_result);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    GTask *                     task;
+    char *                      uri;
+    const char *const *         http_headers;
+    NMHttpClientPollGetCheckFcn check_fcn;
+    gpointer                    check_user_data;
+    GBytes *                    response_data;
+    gsize                       request_max_data;
+    long                        response_code;
+    int                         request_timeout_ms;
+} PollGetData;
+
+static void
+_poll_get_data_free(gpointer data)
+{
+    PollGetData *poll_get_data = data;
+
+    g_free(poll_get_data->uri);
+
+    nm_clear_pointer(&poll_get_data->response_data, g_bytes_unref);
+    g_strfreev((char **) poll_get_data->http_headers);
+
+    nm_g_slice_free(poll_get_data);
+}
+
+static void
+_poll_get_probe_start_fcn(GCancellable *      cancellable,
+                          gpointer            probe_user_data,
+                          GAsyncReadyCallback callback,
+                          gpointer            user_data)
+{
+    PollGetData *poll_get_data = probe_user_data;
+
+    /* balanced by _poll_get_probe_finish_fcn() */
+    g_object_ref(poll_get_data->task);
+
+    nm_http_client_get(g_task_get_source_object(poll_get_data->task),
+                       poll_get_data->uri,
+                       poll_get_data->request_timeout_ms,
+                       poll_get_data->request_max_data,
+                       poll_get_data->http_headers,
+                       cancellable,
+                       callback,
+                       user_data);
+}
+
+static gboolean
+_poll_get_probe_finish_fcn(GObject *     source,
+                           GAsyncResult *result,
+                           gpointer      probe_user_data,
+                           GError **     error)
+{
+    PollGetData *              poll_get_data = probe_user_data;
+    _nm_unused gs_unref_object GTask *task =
+        poll_get_data->task; /* balance ref from _poll_get_probe_start_fcn() */
+    gboolean      success;
+    gs_free_error GError *local_error    = NULL;
+    gs_unref_bytes GBytes *response_data = NULL;
+    long                   response_code = -1;
+
+    success = nm_http_client_get_finish(g_task_get_source_object(poll_get_data->task),
+                                        result,
+                                        &response_code,
+                                        &response_data,
+                                        &local_error);
+
+    nm_assert((!!success) == (!local_error));
+
+    if (local_error) {
+        if (nm_utils_error_is_cancelled(local_error)) {
+            g_propagate_error(error, g_steal_pointer(&local_error));
+            return TRUE;
+        }
+        /* any other error. Continue polling. */
+        return FALSE;
+    }
+
+    if (poll_get_data->check_fcn) {
+        success = poll_get_data->check_fcn(response_code,
+                                           response_data,
+                                           poll_get_data->check_user_data,
+                                           &local_error);
+    } else
+        success = (response_code == 200);
+
+    if (local_error) {
+        g_propagate_error(error, g_steal_pointer(&local_error));
+        return TRUE;
+    }
+
+    if (!success) {
+        /* Not yet ready. Continue polling. */
+        return FALSE;
+    }
+
+    poll_get_data->response_code = response_code;
+    poll_get_data->response_data = g_steal_pointer(&response_data);
+    return TRUE;
+}
+
+static void
+_poll_get_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    PollGetData * poll_get_data = user_data;
+    gs_free_error GError *error = NULL;
+    gboolean              success;
+
+    success = nmcs_utils_poll_finish(result, NULL, &error);
+
+    nm_assert((!!success) == (!error));
+
+    if (error)
+        g_task_return_error(poll_get_data->task, g_steal_pointer(&error));
+    else
+        g_task_return_boolean(poll_get_data->task, TRUE);
+
+    g_object_unref(poll_get_data->task);
+}
+
+void
+nm_http_client_poll_get(NMHttpClient *              self,
+                        const char *                uri,
+                        int                         request_timeout_ms,
+                        gssize                      request_max_data,
+                        int                         poll_timeout_ms,
+                        int                         ratelimit_timeout_ms,
+                        const char *const *         http_headers,
+                        GCancellable *              cancellable,
+                        NMHttpClientPollGetCheckFcn check_fcn,
+                        gpointer                    check_user_data,
+                        GAsyncReadyCallback         callback,
+                        gpointer                    user_data)
+{
+    nm_auto_pop_gmaincontext GMainContext *context = NULL;
+    PollGetData *                          poll_get_data;
+
+    g_return_if_fail(NM_IS_HTTP_CLIENT(self));
+    g_return_if_fail(uri && uri[0]);
+    g_return_if_fail(request_timeout_ms >= -1);
+    g_return_if_fail(request_max_data >= -1);
+    g_return_if_fail(poll_timeout_ms >= -1);
+    g_return_if_fail(ratelimit_timeout_ms >= -1);
+    g_return_if_fail(!cancellable || G_CANCELLABLE(cancellable));
+
+    poll_get_data  = g_slice_new(PollGetData);
+    *poll_get_data = (PollGetData){
+        .task = nm_g_task_new(self, cancellable, nm_http_client_poll_get, callback, user_data),
+        .uri  = g_strdup(uri),
+        .request_timeout_ms = request_timeout_ms,
+        .request_max_data   = request_max_data,
+        .check_fcn          = check_fcn,
+        .check_user_data    = check_user_data,
+        .response_code      = -1,
+        .http_headers       = NM_CAST_STRV_CC(g_strdupv((char **) http_headers)),
+    };
+
+    nmcs_wait_for_objects_register(poll_get_data->task);
+
+    g_task_set_task_data(poll_get_data->task, poll_get_data, _poll_get_data_free);
+
+    context =
+        nm_g_main_context_push_thread_default_if_necessary(nm_http_client_get_main_context(self));
+
+    nmcs_utils_poll(poll_timeout_ms,
+                    ratelimit_timeout_ms,
+                    0,
+                    _poll_get_probe_start_fcn,
+                    _poll_get_probe_finish_fcn,
+                    poll_get_data,
+                    cancellable,
+                    _poll_get_done_cb,
+                    poll_get_data);
+}
+
+gboolean
+nm_http_client_poll_get_finish(NMHttpClient *self,
+                               GAsyncResult *result,
+                               long *        out_response_code,
+                               GBytes **     out_response_data,
+                               GError **     error)
+{
+    PollGetData * poll_get_data;
+    GTask *       task;
+    gboolean      success;
+    gs_free_error GError *local_error = NULL;
+
+    g_return_val_if_fail(NM_HTTP_CLIENT(self), FALSE);
+    g_return_val_if_fail(nm_g_task_is_valid(result, self, nm_http_client_poll_get), FALSE);
+
+    task = G_TASK(result);
+
+    success = g_task_propagate_boolean(task, &local_error);
+
+    nm_assert((!!success) == (!local_error));
+
+    if (local_error) {
+        g_propagate_error(error, g_steal_pointer(&local_error));
+        NM_SET_OUT(out_response_code, -1);
+        NM_SET_OUT(out_response_data, NULL);
+        return FALSE;
+    }
+
+    poll_get_data = g_task_get_task_data(task);
+
+    NM_SET_OUT(out_response_code, poll_get_data->response_code);
+    NM_SET_OUT(out_response_data, g_steal_pointer(&poll_get_data->response_data));
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+_mhandle_action(NMHttpClient *self, int sockfd, int ev_bitmask)
+{
+    NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE(self);
+    EHandleData *        edata;
+    CURLMsg *            msg;
+    CURLcode             eret;
+    int                  m_left;
+    CURLMcode            ret;
+    int                  running_handles;
+
+    ret = curl_multi_socket_action(priv->mhandle, sockfd, ev_bitmask, &running_handles);
+    if (ret != CURLM_OK) {
+        _LOGE("curl: curl_multi_socket_action() failed: (%d) %s", ret, curl_multi_strerror(ret));
+        /* really unexpected. Not clear how to handle this. */
+    }
+
+    while ((msg = curl_multi_info_read(priv->mhandle, &m_left))) {
+        if (msg->msg != CURLMSG_DONE)
+            continue;
+
+        eret = curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, (char **) &edata);
+
+        nm_assert(eret == CURLE_OK);
+        nm_assert(edata);
+
+        edata->ehandle_result = msg->data.result;
+        _ehandle_complete(edata, NULL);
+    }
+}
+
+static gboolean
+_mhandle_socket_cb(int fd, GIOCondition condition, gpointer user_data)
+{
+    int ev_bitmask = 0;
+
+    if (condition & G_IO_IN)
+        ev_bitmask |= CURL_CSELECT_IN;
+    if (condition & G_IO_OUT)
+        ev_bitmask |= CURL_CSELECT_OUT;
+    if (condition & G_IO_ERR)
+        ev_bitmask |= CURL_CSELECT_ERR;
+
+    _mhandle_action(user_data, fd, ev_bitmask);
+    return G_SOURCE_CONTINUE;
+}
+
+static int
+_mhandle_socketfunction_cb(CURL *        e_handle,
+                           curl_socket_t fd,
+                           int           what,
+                           void *        user_data,
+                           void *        socketp)
+{
+    GSource *            source_socket;
+    NMHttpClient *       self = user_data;
+    NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE(self);
+
+    (void) _NM_ENSURE_TYPE(int, fd);
+
+    g_hash_table_remove(priv->source_sockets_hashtable, GINT_TO_POINTER(fd));
+
+    if (what != CURL_POLL_REMOVE) {
+        GIOCondition condition = 0;
+
+        if (what == CURL_POLL_IN)
+            condition = G_IO_IN;
+        else if (what == CURL_POLL_OUT)
+            condition = G_IO_OUT;
+        else if (what == CURL_POLL_INOUT)
+            condition = G_IO_IN | G_IO_OUT;
+        else
+            condition = 0;
+
+        if (condition) {
+            source_socket = nm_g_unix_fd_source_new(fd,
+                                                    condition,
+                                                    G_PRIORITY_DEFAULT,
+                                                    _mhandle_socket_cb,
+                                                    self,
+                                                    NULL);
+            g_source_attach(source_socket, priv->context);
+
+            g_hash_table_insert(priv->source_sockets_hashtable, GINT_TO_POINTER(fd), source_socket);
+        }
+    }
+
+    return CURLM_OK;
+}
+
+static gboolean
+_mhandle_timeout_cb(gpointer user_data)
+{
+    _mhandle_action(user_data, CURL_SOCKET_TIMEOUT, 0);
+    return G_SOURCE_REMOVE;
+}
+
+static int
+_mhandle_timerfunction_cb(CURLM *multi, long timeout_msec, void *user_data)
+{
+    NMHttpClient *       self = user_data;
+    NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE(self);
+
+    nm_clear_pointer(&priv->mhandle_source_timeout, nm_g_source_destroy_and_unref);
+    if (timeout_msec >= 0) {
+        priv->mhandle_source_timeout =
+            _source_attach(self,
+                           nm_g_timeout_source_new(NM_MIN(timeout_msec, G_MAXINT),
+                                                   G_PRIORITY_DEFAULT,
+                                                   _mhandle_timeout_cb,
+                                                   self,
+                                                   NULL));
+    }
+    return 0;
+}
+
+/*****************************************************************************/
+
+static void
+nm_http_client_init(NMHttpClient *self)
+{
+    NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE(self);
+
+    priv->source_sockets_hashtable =
+        g_hash_table_new_full(nm_direct_hash,
+                              NULL,
+                              NULL,
+                              (GDestroyNotify) nm_g_source_destroy_and_unref);
+}
+
+static void
+constructed(GObject *object)
+{
+    NMHttpClient *       self = NM_HTTP_CLIENT(object);
+    NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE(self);
+
+    priv->context = g_main_context_ref_thread_default();
+
+    priv->mhandle = curl_multi_init();
+    if (!priv->mhandle)
+        _LOGE("curl: failed to create multi-handle");
+    else {
+        curl_multi_setopt(priv->mhandle, CURLMOPT_SOCKETFUNCTION, _mhandle_socketfunction_cb);
+        curl_multi_setopt(priv->mhandle, CURLMOPT_SOCKETDATA, self);
+        curl_multi_setopt(priv->mhandle, CURLMOPT_TIMERFUNCTION, _mhandle_timerfunction_cb);
+        curl_multi_setopt(priv->mhandle, CURLMOPT_TIMERDATA, self);
+    }
+
+    G_OBJECT_CLASS(nm_http_client_parent_class)->constructed(object);
+}
+
+NMHttpClient *
+nm_http_client_new(void)
+{
+    return g_object_new(NM_TYPE_HTTP_CLIENT, NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMHttpClient *       self = NM_HTTP_CLIENT(object);
+    NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE(self);
+
+    nm_clear_pointer(&priv->mhandle, curl_multi_cleanup);
+    nm_clear_pointer(&priv->source_sockets_hashtable, g_hash_table_unref);
+
+    nm_clear_g_source_inst(&priv->mhandle_source_timeout);
+
+    G_OBJECT_CLASS(nm_http_client_parent_class)->dispose(object);
+}
+
+static void
+finalize(GObject *object)
+{
+    NMHttpClient *       self = NM_HTTP_CLIENT(object);
+    NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE(self);
+
+    G_OBJECT_CLASS(nm_http_client_parent_class)->finalize(object);
+
+    g_main_context_unref(priv->context);
+
+    curl_global_cleanup();
+}
+
+static void
+nm_http_client_class_init(NMHttpClientClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+    object_class->constructed = constructed;
+    object_class->dispose     = dispose;
+    object_class->finalize    = finalize;
+
+    nm_http_client_curl_global_init();
+}