NetworkManager/clients/cloud-setup/nm-http-client.c
Thomas Haller 70971d1141
all: avoid wrong compiler warning about uninitalized variables with LTO
Seems with LTO the compiler can sometimes think that thes variables are
uninitialized. Usually those code paths are only after an assertion was
hit (g_return*()), but we still need to workaround the warning.
2020-08-17 15:18:02 +02:00

813 lines
24 KiB
C

// SPDX-License-Identifier: LGPL-2.1+
#include "nm-default.h"
#include "nm-http-client.h"
#include <curl/curl.h>
#include "nm-cloud-setup-utils.h"
#include "nm-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 ();
}