summary refs log tree commit diff
path: root/clients/cloud-setup/nm-http-client.c
diff options
context:
space:
mode:
Diffstat (limited to 'clients/cloud-setup/nm-http-client.c')
-rw-r--r--clients/cloud-setup/nm-http-client.c746
1 files changed, 746 insertions, 0 deletions
diff --git a/clients/cloud-setup/nm-http-client.c b/clients/cloud-setup/nm-http-client.c
new file mode 100644
index 00000000..94331095
--- /dev/null
+++ b/clients/cloud-setup/nm-http-client.c
@@ -0,0 +1,746 @@
+// SPDX-License-Identifier: LGPL-2.1+
+
+#include "nm-default.h"
+
+#include "nm-http-client.h"
+
+#include <curl/curl.h>
+#include <glib-unix.h>
+
+#include "nm-cloud-setup-utils.h"
+
+#define NM_CURL_DEBUG 0
+
+/*****************************************************************************/
+
+typedef struct {
+	GMainContext *context;
+	CURLM *mhandle;
+	GSource *mhandle_source_timeout;
+	GSource *mhandle_source_socket;
+} 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;
+	GString *recv_data;
+	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);
+
+	if (edata->recv_data)
+		g_string_free (edata->recv_data, TRUE);
+	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, FALSE))
+			_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 (edata->recv_data->str, 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 = g_string_free_to_bytes (g_steal_pointer (&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;
+
+	g_string_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,
+                    GCancellable *cancellable,
+                    GAsyncReadyCallback callback,
+                    gpointer user_data)
+{
+	NMHttpClientPrivate *priv;
+	EHandleData *edata;
+
+	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      = g_string_sized_new (NM_MIN (max_data, 245)),
+		.max_data       = max_data,
+		.url            = g_strdup (url),
+	};
+
+	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 (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;
+	}
+}
+
+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);
+	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;
+	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);
+
+	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,
+	                    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;
+	long response_code;
+	gs_unref_bytes GBytes *response_data = NULL;
+
+	success = nm_http_client_get_finish (g_task_get_source_object (poll_get_data->task),
+	                                     result,
+	                                     &response_code,
+	                                     &response_data,
+	                                     &local_error);
+
+	if (!success) {
+		if (nm_utils_error_is_cancelled (local_error, FALSE)) {
+			g_propagate_error (error, g_steal_pointer (&local_error));
+			return TRUE;
+		}
+		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)
+		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);
+
+	if (error)
+		g_task_return_error (poll_get_data->task, g_steal_pointer (&error));
+	else
+		g_task_return_boolean (poll_get_data->task, success);
+
+	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,
+                         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,
+	};
+
+	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);
+	if (   local_error
+	    || !success) {
+		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)
+{
+	NMHttpClient *self = user_data;
+	NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE (self);
+
+	(void) _NM_ENSURE_TYPE (int, fd);
+
+	nm_clear_g_source_inst (&priv->mhandle_source_socket);
+
+	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) {
+			priv->mhandle_source_socket = g_unix_fd_source_new (fd, condition);
+			g_source_set_callback (priv->mhandle_source_socket, G_SOURCE_FUNC (_mhandle_socket_cb), self, NULL);
+			g_source_attach (priv->mhandle_source_socket, priv->context);
+		}
+	}
+
+	return CURLM_OK;
+}
+
+static gboolean
+_mhandle_timeout_cb (gpointer user_data)
+{
+	_mhandle_action (user_data, CURL_SOCKET_TIMEOUT, 0);
+	return G_SOURCE_CONTINUE;
+}
+
+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)
+{
+}
+
+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);
+		if (NM_CURL_DEBUG)
+			curl_multi_setopt (priv->mhandle, CURLOPT_VERBOSE, 1);
+	}
+
+	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_g_source_inst (&priv->mhandle_source_timeout);
+	nm_clear_g_source_inst (&priv->mhandle_source_socket);
+
+	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 ();
+}