/* * httpclient_native.cpp — libcurl-multi backend. * * Single CURLM handle owned by this module. http_post_json / http_get queue * an easy handle; http_pump() runs curl_multi_perform and drains finished * transfers, firing user callbacks exactly once. * * Failure modes (DNS, connect refused, timeout, etc.) are surfaced as * status=0 in the response — caller doesn't have to distinguish curl error * codes from HTTP errors. */ #include "httpclient.h" #include "kobolog.h" #include #include #include namespace { CURLM *g_multi = nullptr; struct pending_t { http_callback_t cb; void *user; std::string body; curl_slist *headers = nullptr; }; // Keyed by CURL* easy handle so curl_multi_info_read results can be routed. std::unordered_map g_pending; size_t write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) { pending_t *p = static_cast(userdata); p->body.append(ptr, size * nmemb); return size * nmemb; } void start_request(CURL *easy, pending_t *pending) { curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_cb); curl_easy_setopt(easy, CURLOPT_WRITEDATA, pending); curl_easy_setopt(easy, CURLOPT_TIMEOUT, 10L); curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT, 5L); curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(easy, CURLOPT_USERAGENT, "kobodl/0.5.1"); g_pending[easy] = pending; CURLMcode rc = curl_multi_add_handle(g_multi, easy); if(rc != CURLM_OK) { log_printf(ELOG, "http: curl_multi_add_handle failed: %s\n", curl_multi_strerror(rc)); // Synthesize a transport failure callback. http_response_t resp{0, "", 0}; pending->cb(&resp, pending->user); g_pending.erase(easy); if(pending->headers) curl_slist_free_all(pending->headers); curl_easy_cleanup(easy); delete pending; } } } // namespace extern "C" void http_init(void) { if(g_multi) return; curl_global_init(CURL_GLOBAL_DEFAULT); g_multi = curl_multi_init(); } extern "C" void http_shutdown(void) { if(!g_multi) return; // Abandon any in-flight handles — we're shutting down, callbacks are // moot. Free the resources to avoid leak reports. for(auto &kv : g_pending) { curl_multi_remove_handle(g_multi, kv.first); if(kv.second->headers) curl_slist_free_all(kv.second->headers); curl_easy_cleanup(kv.first); delete kv.second; } g_pending.clear(); curl_multi_cleanup(g_multi); g_multi = nullptr; curl_global_cleanup(); } extern "C" void http_post_json(const char *url, const char *body, http_callback_t cb, void *user) { if(!g_multi || !cb) return; CURL *easy = curl_easy_init(); if(!easy) { http_response_t resp{0, "", 0}; cb(&resp, user); return; } auto *pending = new pending_t; pending->cb = cb; pending->user = user; pending->headers = curl_slist_append(nullptr, "Content-Type: application/json"); curl_easy_setopt(easy, CURLOPT_URL, url); curl_easy_setopt(easy, CURLOPT_POST, 1L); curl_easy_setopt(easy, CURLOPT_COPYPOSTFIELDS, body ? body : ""); curl_easy_setopt(easy, CURLOPT_HTTPHEADER, pending->headers); start_request(easy, pending); } extern "C" void http_get(const char *url, http_callback_t cb, void *user) { if(!g_multi || !cb) return; CURL *easy = curl_easy_init(); if(!easy) { http_response_t resp{0, "", 0}; cb(&resp, user); return; } auto *pending = new pending_t; pending->cb = cb; pending->user = user; curl_easy_setopt(easy, CURLOPT_URL, url); curl_easy_setopt(easy, CURLOPT_HTTPGET, 1L); start_request(easy, pending); } extern "C" void http_pump(void) { if(!g_multi) return; int running = 0; curl_multi_perform(g_multi, &running); CURLMsg *msg; int msgs_left = 0; while((msg = curl_multi_info_read(g_multi, &msgs_left)) != nullptr) { if(msg->msg != CURLMSG_DONE) continue; CURL *easy = msg->easy_handle; auto it = g_pending.find(easy); if(it == g_pending.end()) { curl_multi_remove_handle(g_multi, easy); curl_easy_cleanup(easy); continue; } pending_t *p = it->second; long status = 0; if(msg->data.result == CURLE_OK) curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &status); else log_printf(DLOG, "http: transport error: %s\n", curl_easy_strerror(msg->data.result)); http_response_t resp; resp.status = (int)status; resp.body = p->body.c_str(); resp.len = p->body.size(); p->cb(&resp, p->user); curl_multi_remove_handle(g_multi, easy); if(p->headers) curl_slist_free_all(p->headers); curl_easy_cleanup(easy); g_pending.erase(it); delete p; } }