NetworkManager/clients/cloud-setup/nm-http-client.c
Lubomir Rintel 6f1f802357 cloud-setup: actually pass the HTTP method in nm_http_client_poll_req()
https://bugzilla.redhat.com/show_bug.cgi?id=2179718

Conflicts: code format, missing 599fe234ea ("cloud-setup: use
nm_strv_dup_packed() in nm_http_client_poll_get()")

Fixes: 8b7e12c2d6 ('cloud-setup/ec2: start with requesting a IMDSv2 token')
Fixes: cd74d75002 ('cloud-setup: make nm_http_client_req() accept a method argument')
(cherry picked from commit f07da04cd9)
(cherry picked from commit d787c0c59d)
(cherry picked from commit 6abbdaaa64)
(cherry picked from commit 16dc184845)
(cherry picked from commit 35e76b509b)
(cherry picked from commit b369cdc5f2)
(cherry picked from commit 6e1566ffaf)
(cherry picked from commit 4a082baf82)
(cherry picked from commit ec4f7d540d)
(cherry picked from commit ce533baa1a)
(cherry picked from commit 35e232f9c2)
2023-09-20 08:25:07 +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 <glib-unix.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, 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);
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,
const char * http_method,
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 %s ...", http_method ?: "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 (http_method)
curl_easy_setopt(edata->ehandle, CURLOPT_CUSTOMREQUEST, http_method);
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;
const char *http_method;
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_strfreev ((char **) 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,
poll_req_data->http_method,
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;
long response_code;
gs_unref_bytes GBytes *response_data = NULL;
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, FALSE)) {
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,
const char *http_method,
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 = NM_CAST_STRV_CC (g_strdupv ((char **) http_headers)),
.http_method = http_method,
};
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 = g_unix_fd_source_new (fd, condition);
g_source_set_callback (source_socket, G_SOURCE_FUNC (_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 ();
}