/* SPDX-License-Identifier: LGPL-2.1-or-later */
#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();
}