/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include "curl-util.h"
#include "log.h"

#if HAVE_LIBCURL

#include "sd-dlopen.h"
#include "sd-event.h"

#include "alloc-util.h"
#include "dlfcn-util.h"
#include "fd-util.h"
#include "hashmap.h"
#include "set.h"
#include "string-util.h"
#include "strv.h"
#include "time-util.h"
#include "version.h"

DLSYM_PROTOTYPE(curl_easy_cleanup) = NULL;
DLSYM_PROTOTYPE(curl_easy_getinfo) = NULL;
DLSYM_PROTOTYPE(curl_easy_init) = NULL;
DLSYM_PROTOTYPE(curl_easy_perform) = NULL;
DLSYM_PROTOTYPE(curl_easy_setopt) = NULL;
DLSYM_PROTOTYPE(curl_easy_strerror) = NULL;
#if LIBCURL_VERSION_NUM >= 0x075300
DLSYM_PROTOTYPE(curl_easy_header) = NULL;
#endif
DLSYM_PROTOTYPE(curl_getdate) = NULL;
static DLSYM_PROTOTYPE(curl_multi_add_handle) = NULL;
static DLSYM_PROTOTYPE(curl_multi_assign) = NULL;
static DLSYM_PROTOTYPE(curl_multi_cleanup) = NULL;
static DLSYM_PROTOTYPE(curl_multi_info_read) = NULL;
static DLSYM_PROTOTYPE(curl_multi_init) = NULL;
static DLSYM_PROTOTYPE(curl_multi_remove_handle) = NULL;
static DLSYM_PROTOTYPE(curl_multi_setopt) = NULL;
static DLSYM_PROTOTYPE(curl_multi_socket_action) = NULL;
DLSYM_PROTOTYPE(curl_slist_append) = NULL;
DLSYM_PROTOTYPE(curl_slist_free_all) = NULL;

DEFINE_TRIVIAL_CLEANUP_FUNC_FULL_RENAME(CURLM*, sym_curl_multi_cleanup, curl_multi_cleanupp, NULL);

struct CurlGlue {
        unsigned n_ref;
        sd_event *event;
        CURLM *curl;
        sd_event_source *timer;
        Hashmap *ios;
        sd_event_source *defer;
        Set *slots;             /* CurlSlot* — back-pointer set; floating slots are kept alive here */
};

struct CurlSlot {
        unsigned n_ref;
        CurlGlue *glue;         /* NULL once disconnected (callback fired, cancelled, or glue died) */
        CURL *easy;             /* owned; cleared once the easy handle has been freed */
        bool floating;
        curl_finished_t callback;
        void *userdata;
};

static void curl_slot_disconnect(CurlSlot *slot, bool unref) {
        assert(slot);

        /* Tear down the slot's connection to the glue: pull the easy handle out of the multi,
         * curl_easy_cleanup() it, and remove the slot from the glue's lookup set. Floating
         * slots are owned by that set, so on disconnect we drop the implicit ref (when
         * unref=true; the recursive call from curl_slot_free passes false to avoid infinite
         * recursion). Non-floating slots release the back-ref they held on the glue.
         *
         * Idempotent: once slot->glue is NULL, subsequent calls are no-ops. */

        if (!slot->glue)
                return;

        CurlGlue *glue = slot->glue;

        if (slot->easy) {
                if (glue->curl)
                        (void) sym_curl_multi_remove_handle(glue->curl, slot->easy);
                sym_curl_easy_cleanup(slot->easy);
                slot->easy = NULL;
        }

        set_remove(glue->slots, slot);
        slot->glue = NULL;

        if (!slot->floating)
                curl_glue_unref(glue);
        else if (unref)
                curl_slot_unref(slot);
}

static CurlSlot* curl_slot_free(CurlSlot *slot) {
        if (!slot)
                return NULL;

        curl_slot_disconnect(slot, /* unref= */ false);
        return mfree(slot);
}

DEFINE_TRIVIAL_REF_UNREF_FUNC(CurlSlot, curl_slot, curl_slot_free);

CURL* curl_slot_get_easy(CurlSlot *slot) {
        assert(slot);
        return slot->easy;
}

CurlGlue* curl_slot_get_glue(CurlSlot *slot) {
        assert(slot);
        return slot->glue;
}

static void curl_glue_check_finished(CurlGlue *g) {
        int r;

        assert(g);

        /* sd_event_get_exit_code() returns -ENODATA if no exit was scheduled yet */
        r = sd_event_get_exit_code(g->event, /* ret= */ NULL);
        if (r >= 0)
                return; /* exit scheduled? Then don't process this anymore */
        if (r != -ENODATA)
                log_debug_errno(r, "Unexpected error while checking for event loop exit code, ignoring: %m");

        CURLMsg *msg;
        int k = 0;
        msg = sym_curl_multi_info_read(g->curl, &k);
        if (!msg)
                return;

        if (msg->msg == CURLMSG_DONE) {
                CURL *easy = msg->easy_handle;
                CURLcode code = msg->data.result;
                CurlSlot *slot = NULL;

                if (sym_curl_easy_getinfo(easy, CURLINFO_PRIVATE, (char**) &slot) == CURLE_OK && slot) {
                        /* Pin the slot across the callback: a floating slot's only
                         * reference is the one held via the glue's slots set, and
                         * disconnect drops it. */
                        curl_slot_ref(slot);

                        if (slot->callback) {
                                r = slot->callback(slot, easy, code, slot->userdata);
                                if (r < 0)
                                        log_debug_errno(r, "Curl finished callback returned error, ignoring: %m");
                        }

                        curl_slot_disconnect(slot, /* unref= */ true);
                        curl_slot_unref(slot);
                }
        }

        /* This is a queue, process another item soon, but do so in a later event loop iteration. */
        (void) sd_event_source_set_enabled(g->defer, SD_EVENT_ONESHOT);
}

static int curl_glue_on_io(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
        CurlGlue *g = ASSERT_PTR(userdata);
        int action, k = 0;

        assert(s);

        if (FLAGS_SET(revents, EPOLLIN | EPOLLOUT))
                action = CURL_POLL_INOUT;
        else if (revents & EPOLLIN)
                action = CURL_POLL_IN;
        else if (revents & EPOLLOUT)
                action = CURL_POLL_OUT;
        else
                action = 0;

        if (sym_curl_multi_socket_action(g->curl, fd, action, &k) != CURLM_OK)
                return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
                                       "Failed to propagate IO event.");

        curl_glue_check_finished(g);
        return 0;
}

static int curl_glue_socket_callback(CURL *curl, curl_socket_t s, int action, void *userdata, void *socketp) {
        sd_event_source *io = socketp;
        CurlGlue *g = ASSERT_PTR(userdata);
        uint32_t events = 0;
        int r;

        assert(curl);

        if (action == CURL_POLL_REMOVE) {
                if (io) {
                        sd_event_source_disable_unref(io);

                        hashmap_remove(g->ios, FD_TO_PTR(s));
                }

                return 0;
        }

        /* Don't configure io event source anymore when the event loop is dead already. */
        if (g->event && sd_event_get_state(g->event) == SD_EVENT_FINISHED)
                return 0;

        r = hashmap_ensure_allocated(&g->ios, &trivial_hash_ops);
        if (r < 0) {
                log_oom();
                return -1;
        }

        if (action == CURL_POLL_IN)
                events = EPOLLIN;
        else if (action == CURL_POLL_OUT)
                events = EPOLLOUT;
        else if (action == CURL_POLL_INOUT)
                events = EPOLLIN|EPOLLOUT;

        if (io) {
                if (sd_event_source_set_io_events(io, events) < 0)
                        return -1;

                if (sd_event_source_set_enabled(io, SD_EVENT_ON) < 0)
                        return -1;
        } else {
                if (sd_event_add_io(g->event, &io, s, events, curl_glue_on_io, g) < 0)
                        return -1;

                if (sym_curl_multi_assign(g->curl, s, io) != CURLM_OK)
                        return -1;

                (void) sd_event_source_set_description(io, "curl-io");

                r = hashmap_put(g->ios, FD_TO_PTR(s), io);
                if (r < 0) {
                        log_oom();
                        sd_event_source_unref(io);
                        return -1;
                }
        }

        return 0;
}

static int curl_glue_on_timer(sd_event_source *s, uint64_t usec, void *userdata) {
        CurlGlue *g = ASSERT_PTR(userdata);
        int k = 0;

        assert(s);

        if (sym_curl_multi_socket_action(g->curl, CURL_SOCKET_TIMEOUT, 0, &k) != CURLM_OK)
                return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
                                       "Failed to propagate timeout.");

        curl_glue_check_finished(g);
        return 0;
}

static int curl_glue_timer_callback(CURLM *curl, long timeout_ms, void *userdata) {
        CurlGlue *g = ASSERT_PTR(userdata);
        usec_t usec;

        assert(curl);

        /* Don't configure timer anymore when the event loop is dead already. */
        if (g->timer) {
                sd_event *event_loop = sd_event_source_get_event(g->timer);
                if (event_loop && sd_event_get_state(event_loop) == SD_EVENT_FINISHED)
                        return 0;
        }

        if (timeout_ms < 0) {
                if (sd_event_source_set_enabled(g->timer, SD_EVENT_OFF) < 0)
                        return -1;

                return 0;
        }

        usec = (usec_t) timeout_ms * USEC_PER_MSEC + USEC_PER_MSEC - 1;

        if (g->timer) {
                if (sd_event_source_set_time_relative(g->timer, usec) < 0)
                        return -1;

                if (sd_event_source_set_enabled(g->timer, SD_EVENT_ONESHOT) < 0)
                        return -1;
        } else {
                if (sd_event_add_time_relative(g->event, &g->timer, CLOCK_BOOTTIME, usec, 0, curl_glue_on_timer, g) < 0)
                        return -1;

                (void) sd_event_source_set_description(g->timer, "curl-timer");
        }

        return 0;
}

static int curl_glue_on_defer(sd_event_source *s, void *userdata) {
        CurlGlue *g = ASSERT_PTR(userdata);

        assert(s);

        curl_glue_check_finished(g);
        return 0;
}

static CurlGlue* curl_glue_free(CurlGlue *g) {
        sd_event_source *io;
        CurlSlot *slot;

        if (!g)
                return NULL;

        /* Drain any slots still hanging off us. By construction only floating slots can
         * be here: connected non-floating slots hold a glue back-ref, so glue's last ref
         * couldn't have dropped while one was attached. disconnect(unref=true) does the
         * floating slot's free as part of its work. set_steal_first() pops up front so
         * forward progress doesn't depend on disconnect's internal set_remove(). */
        while ((slot = set_steal_first(g->slots)))
                curl_slot_disconnect(slot, /* unref= */ true);
        g->slots = set_free(g->slots);

        if (g->curl)
                sym_curl_multi_cleanup(g->curl);

        while ((io = hashmap_steal_first(g->ios)))
                sd_event_source_unref(io);

        hashmap_free(g->ios);

        sd_event_source_disable_unref(g->timer);
        sd_event_source_disable_unref(g->defer);
        sd_event_unref(g->event);
        return mfree(g);
}

DEFINE_TRIVIAL_REF_UNREF_FUNC(CurlGlue, curl_glue, curl_glue_free);

int curl_glue_new(CurlGlue **glue, sd_event *event) {
        _cleanup_(curl_glue_unrefp) CurlGlue *g = NULL;
        _cleanup_(curl_multi_cleanupp) CURLM *c = NULL;
        _cleanup_(sd_event_unrefp) sd_event *e = NULL;
        int r;

        assert(glue);

        r = dlopen_curl(LOG_DEBUG);
        if (r < 0)
                return r;

        if (event)
                e = sd_event_ref(event);
        else {
                r = sd_event_default(&e);
                if (r < 0)
                        return r;
        }

        c = sym_curl_multi_init();
        if (!c)
                return -ENOMEM;

        g = new(CurlGlue, 1);
        if (!g)
                return -ENOMEM;

        *g = (CurlGlue) {
                .n_ref = 1,
                .event = TAKE_PTR(e),
                .curl = TAKE_PTR(c),
        };

        if (sym_curl_multi_setopt(g->curl, CURLMOPT_SOCKETDATA, g) != CURLM_OK)
                return -EINVAL;

        if (sym_curl_multi_setopt(g->curl, CURLMOPT_SOCKETFUNCTION, curl_glue_socket_callback) != CURLM_OK)
                return -EINVAL;

        if (sym_curl_multi_setopt(g->curl, CURLMOPT_TIMERDATA, g) != CURLM_OK)
                return -EINVAL;

        if (sym_curl_multi_setopt(g->curl, CURLMOPT_TIMERFUNCTION, curl_glue_timer_callback) != CURLM_OK)
                return -EINVAL;

        r = sd_event_add_defer(g->event, &g->defer, curl_glue_on_defer, g);
        if (r < 0)
                return r;

        (void) sd_event_source_set_description(g->defer, "curl-defer");

        *glue = TAKE_PTR(g);

        return 0;
}

int curl_glue_make(CURL **ret, const char *url) {
        _cleanup_(curl_easy_cleanupp) CURL *c = NULL;
        const char *useragent;
        int r;

        assert(ret);
        assert(url);

        r = dlopen_curl(LOG_DEBUG);
        if (r < 0)
                return r;

        c = sym_curl_easy_init();
        if (!c)
                return -ENOMEM;

        if (DEBUG_LOGGING)
                (void) sym_curl_easy_setopt(c, CURLOPT_VERBOSE, 1L);

        if (sym_curl_easy_setopt(c, CURLOPT_URL, url) != CURLE_OK)
                return -EIO;

        useragent = strjoina(program_invocation_short_name, "/" GIT_VERSION);
        if (sym_curl_easy_setopt(c, CURLOPT_USERAGENT, useragent) != CURLE_OK)
                return -EIO;

        if (sym_curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L) != CURLE_OK)
                return -EIO;

        if (sym_curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L) != CURLE_OK)
                return -EIO;

        if (sym_curl_easy_setopt(c, CURLOPT_LOW_SPEED_TIME, 60L) != CURLE_OK)
                return -EIO;

        if (sym_curl_easy_setopt(c, CURLOPT_LOW_SPEED_LIMIT, 30L) != CURLE_OK)
                return -EIO;

#if LIBCURL_VERSION_NUM >= 0x075500 /* libcurl 7.85.0 */
        if (sym_curl_easy_setopt(c, CURLOPT_PROTOCOLS_STR, "HTTP,HTTPS,FILE") != CURLE_OK)
#else
        if (sym_curl_easy_setopt(c, CURLOPT_PROTOCOLS, CURLPROTO_HTTP|CURLPROTO_HTTPS|CURLPROTO_FILE) != CURLE_OK)
                return -EIO;
        if (sym_curl_easy_setopt(c, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP|CURLPROTO_HTTPS) != CURLE_OK)
#endif
                return -EIO;

        *ret = TAKE_PTR(c);
        return 0;
}

int curl_glue_perform_async(
                CurlGlue *g,
                CURL *easy,
                curl_finished_t cb,
                void *userdata,
                CurlSlot **ret_slot) {

        int r;

        assert(g);
        assert(easy);

        _cleanup_(curl_slot_unrefp) CurlSlot *slot = new(CurlSlot, 1);
        if (!slot)
                return -ENOMEM;

        *slot = (CurlSlot) {
                .n_ref = 1,
                .glue = NULL,    /* wired up below, after we've committed to the multi */
                .easy = easy,
                .floating = !ret_slot,
                .callback = cb,
                .userdata = userdata,
        };

        r = set_ensure_put(&g->slots, &trivial_hash_ops, slot);
        if (r < 0)
                return r;
        assert(r > 0);

        if (sym_curl_multi_add_handle(g->curl, easy) != CURLM_OK) {
                set_remove(g->slots, slot);
                return -EIO;
        }

        /* Stash the slot pointer on the easy handle so curl_glue_check_finished() can recover
         * it on completion. Set this only after we've fully committed to the multi, so that
         * error paths above don't leave a dangling pointer on the easy handle. */
        if (sym_curl_easy_setopt(easy, CURLOPT_PRIVATE, slot) != CURLE_OK) {
                sym_curl_multi_remove_handle(g->curl, easy);
                set_remove(g->slots, slot);
                return -EIO;
        }

        slot->glue = g;
        if (!slot->floating)
                curl_glue_ref(g);

        /* Transfer the slot's single reference: to the caller for non-floating slots, or to
         * the glue's slot set (implicitly, until disconnect drops it) for floating ones. */
        if (ret_slot)
                *ret_slot = slot;

        TAKE_PTR(slot);
        return 0;
}

struct curl_slist *curl_slist_new(const char *first, ...) {
        struct curl_slist *l;
        va_list ap;

        if (!first)
                return NULL;

        l = sym_curl_slist_append(NULL, first);
        if (!l)
                return NULL;

        va_start(ap, first);

        for (;;) {
                struct curl_slist *n;
                const char *i;

                i = va_arg(ap, const char*);
                if (!i)
                        break;

                n = sym_curl_slist_append(l, i);
                if (!n) {
                        va_end(ap);
                        sym_curl_slist_free_all(l);
                        return NULL;
                }

                l = n;
        }

        va_end(ap);
        return l;
}

int curl_header_strdup(const void *contents, size_t sz, const char *field, char **value) {
        const char *p;
        char *s;

        assert(value);

        p = memory_startswith_no_case(contents, sz, field);
        if (!p)
                return 0;

        sz -= p - (const char*) contents;

        if (memchr(p, 0, sz))
                return 0;

        /* Skip over preceding whitespace */
        while (sz > 0 && strchr(WHITESPACE, p[0])) {
                p++;
                sz--;
        }

        /* Truncate trailing whitespace */
        while (sz > 0 && strchr(WHITESPACE, p[sz-1]))
                sz--;

        s = strndup(p, sz);
        if (!s)
                return -ENOMEM;

        *value = s;
        return 1;
}

int curl_parse_http_time(const char *t, usec_t *ret) {
        assert(t);
        assert(ret);

        time_t v = sym_curl_getdate(t, NULL);
        if (v == (time_t) -1)
                return -EINVAL;

        if ((usec_t) v >= USEC_INFINITY / USEC_PER_SEC) /* check overflow */
                return -ERANGE;

        *ret = (usec_t) v * USEC_PER_SEC;

        return 0;
}

int curl_append_to_header(struct curl_slist **list, char **headers) {
        /* This function leaves 'list' modified on partial failure.
         * Input/output param list may point to NULL. */

        assert(list);

        STRV_FOREACH(h, headers) {
                struct curl_slist *l = sym_curl_slist_append(*list, *h);
                if (!l)
                        return -ENOMEM;
                *list = l;
        }

        return 0;
}

#endif

int dlopen_curl(int log_level) {
#if HAVE_LIBCURL
        static void *curl_dl = NULL;

        SD_ELF_NOTE_DLOPEN(
                        "curl",
                        "Support for downloading and uploading files over HTTP",
                        SD_ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED,
                        "libcurl.so.4");

        return dlopen_many_sym_or_warn(
                        &curl_dl,
                        "libcurl.so.4",
                        log_level,
                        DLSYM_ARG(curl_easy_cleanup),
                        DLSYM_ARG(curl_easy_getinfo),
                        DLSYM_ARG(curl_easy_init),
                        DLSYM_ARG(curl_easy_perform),
                        DLSYM_ARG(curl_easy_setopt),
                        DLSYM_ARG(curl_easy_strerror),
#if LIBCURL_VERSION_NUM >= 0x075300
                        DLSYM_ARG(curl_easy_header),
#endif
                        DLSYM_ARG(curl_getdate),
                        DLSYM_ARG(curl_multi_add_handle),
                        DLSYM_ARG(curl_multi_assign),
                        DLSYM_ARG(curl_multi_cleanup),
                        DLSYM_ARG(curl_multi_info_read),
                        DLSYM_ARG(curl_multi_init),
                        DLSYM_ARG(curl_multi_remove_handle),
                        DLSYM_ARG(curl_multi_setopt),
                        DLSYM_ARG(curl_multi_socket_action),
                        DLSYM_ARG(curl_slist_append),
                        DLSYM_ARG(curl_slist_free_all));
#else
        return log_full_errno(log_level, SYNTHETIC_ERRNO(EOPNOTSUPP),
                              "libcurl support is not compiled in.");
#endif
}
