NetworkManager/clients/cloud-setup/nm-http-client.c
Lubomir Rintel 3a0f3ff46f cloud-setup: rename get/Get identifiers to req and Req
We're going to extend those to issue methods other than GET.
Also, "request" would've been too long, "req" looks nicer.

Conflicts: code formatting, missing trivial commit in,
provider-azure, missing 494819bbbf ("cloud-setup: move common
code for get_config() to base class and improve cancellation")

(cherry picked from commit 85ce088616)
(cherry picked from commit 6e8cfbae32)
(cherry picked from commit 20cd11ee49)
(cherry picked from commit 9ce530fa7a)
(cherry picked from commit d6d161a31d)
(cherry picked from commit 977fc2c8c5)
(cherry picked from commit 89ee76409b)
(cherry picked from commit e3d23c7ee0)
2023-09-19 12:52:41 +02:00

791 lines
23 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"
#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
_req_result_free (gpointer data)
{
GetResult *req_result = data;
g_bytes_unref (req_result->response_data);
nm_g_slice_free (req_result);
}
typedef struct {
GTask *task;
GSource *timeout_source;
CURLcode ehandle_result;
CURL *ehandle;
char *url;
GString *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);
if (edata->recv_data)
g_string_free (edata->recv_data, TRUE);
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 *req_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 (edata->recv_data->str, edata->recv_data->len, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL, &str_tmp_1));
_ehandle_free_ehandle (edata);
req_result = g_slice_new (GetResult);
*req_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, req_result, _req_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);
}
static void
nm_http_client_req (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_req, callback, user_data),
.recv_data = g_string_sized_new (NM_MIN (max_data, 245)),
.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;
}
}
static gboolean
nm_http_client_req_finish (NMHttpClient *self,
GAsyncResult *result,
long *out_response_code,
GBytes **out_response_data,
GError **error)
{
GetResult *req_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_req), FALSE);
req_result = g_task_propagate_pointer (G_TASK (result), error);
if (!req_result) {
NM_SET_OUT (out_response_code, -1);
NM_SET_OUT (out_response_data, NULL);
return FALSE;
}
NM_SET_OUT (out_response_code, req_result->response_code);
/* response_data is binary, but is also guaranteed to be NUL terminated! */
NM_SET_OUT (out_response_data, g_steal_pointer (&req_result->response_data));
_req_result_free (req_result);
return TRUE;
}
/*****************************************************************************/
typedef struct {
GTask *task;
char *uri;
const char *const *http_headers;
NMHttpClientPollReqCheckFcn check_fcn;
gpointer check_user_data;
GBytes *response_data;
gsize request_max_data;
long response_code;
int request_timeout_ms;
} PollReqData;
static void
_poll_req_data_free (gpointer data)
{
PollReqData *poll_req_data = data;
g_free (poll_req_data->uri);
nm_clear_pointer (&poll_req_data->response_data, g_bytes_unref);
g_free((gpointer) poll_req_data->http_headers);
nm_g_slice_free (poll_req_data);
}
static void
_poll_req_probe_start_fcn (GCancellable *cancellable,
gpointer probe_user_data,
GAsyncReadyCallback callback,
gpointer user_data)
{
PollReqData *poll_req_data = probe_user_data;
/* balanced by _poll_req_probe_finish_fcn() */
g_object_ref (poll_req_data->task);
nm_http_client_req (g_task_get_source_object (poll_req_data->task),
poll_req_data->uri,
poll_req_data->request_timeout_ms,
poll_req_data->request_max_data,
poll_req_data->http_headers,
cancellable,
callback,
user_data);
}
static gboolean
_poll_req_probe_finish_fcn (GObject *source,
GAsyncResult *result,
gpointer probe_user_data,
GError **error)
{
PollReqData *poll_req_data = probe_user_data;
_nm_unused gs_unref_object GTask *task = poll_req_data->task; /* balance ref from _poll_req_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_req_finish (g_task_get_source_object (poll_req_data->task),
result,
&response_code,
&response_data,
&local_error);
if (!success) {
if (nm_utils_error_is_cancelled (local_error)) {
g_propagate_error (error, g_steal_pointer (&local_error));
return TRUE;
}
return FALSE;
}
if (poll_req_data->check_fcn) {
success = poll_req_data->check_fcn (response_code,
response_data,
poll_req_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_req_data->response_code = response_code;
poll_req_data->response_data = g_steal_pointer (&response_data);
return TRUE;
}
static void
_poll_req_done_cb (GObject *source,
GAsyncResult *result,
gpointer user_data)
{
PollReqData *poll_req_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_req_data->task, g_steal_pointer (&error));
else
g_task_return_boolean (poll_req_data->task, success);
g_object_unref (poll_req_data->task);
}
void
nm_http_client_poll_req (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,
NMHttpClientPollReqCheckFcn check_fcn,
gpointer check_user_data,
GAsyncReadyCallback callback,
gpointer user_data)
{
nm_auto_pop_gmaincontext GMainContext *context = NULL;
PollReqData *poll_req_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_req_data = g_slice_new (PollReqData);
*poll_req_data = (PollReqData) {
.task = nm_g_task_new (self, cancellable, nm_http_client_poll_req, 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 = NULL,
};
if (http_headers) {
poll_req_data->http_headers =
nm_utils_strv_dup_packed(http_headers, -1) ?: g_new(const char *, 1);
}
nmcs_wait_for_objects_register (poll_req_data->task);
g_task_set_task_data (poll_req_data->task,
poll_req_data,
_poll_req_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_req_probe_start_fcn,
_poll_req_probe_finish_fcn,
poll_req_data,
cancellable,
_poll_req_done_cb,
poll_req_data);
}
gboolean
nm_http_client_poll_req_finish (NMHttpClient *self,
GAsyncResult *result,
long *out_response_code,
GBytes **out_response_data,
GError **error)
{
PollReqData *poll_req_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_req), 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_req_data = g_task_get_task_data (task);
NM_SET_OUT (out_response_code, poll_req_data->response_code);
NM_SET_OUT (out_response_data, g_steal_pointer (&poll_req_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 ();
}