diff options
Diffstat (limited to 'src/nm-cloud-setup/nm-http-client.c')
| -rw-r--r-- | src/nm-cloud-setup/nm-http-client.c | 801 |
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(); +} |