commit d29c8dfcc9f379623a155e46655a328b2aa0efcf Author: Slavasil Date: Thu Nov 14 16:48:24 2024 +0300 add basic HTTP client and its testing in main() diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae16e2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/CMakeFiles/ +/CMakeCache.txt +/build/ +/.cache/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b2802bf --- /dev/null +++ b/.gitmodules @@ -0,0 +1,12 @@ +[submodule "td"] + path = td + url = git@github.com:tdlib/td.git +[submodule "libuv"] + path = libuv + url = git@github.com:libuv/libuv.git +[submodule "spdlog"] + path = spdlog + url = git@github.com:gabime/spdlog.git +[submodule "curl"] + path = curl + url = git@github.com:curl/curl.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..9d316cb --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.5) +project(mmcs-quotes-bridge LANGUAGES CXX) + +add_subdirectory(curl) +add_subdirectory(libuv) +add_subdirectory(spdlog) +add_subdirectory(td) + +add_executable(${PROJECT_NAME} main.cpp http.cpp) + +target_link_libraries(${PROJECT_NAME} PRIVATE CURL::libcurl uv spdlog::spdlog Td::TdStatic $<$:ws2_32>) diff --git a/curl b/curl new file mode 160000 index 0000000..acc73ed --- /dev/null +++ b/curl @@ -0,0 +1 @@ +Subproject commit acc73edce8c08e24ceadb4f46d2d812cf03fc6bb diff --git a/http.cpp b/http.cpp new file mode 100644 index 0000000..af53837 --- /dev/null +++ b/http.cpp @@ -0,0 +1,182 @@ +#include "http.h" +#include "curl/curl.h" +#include "curl/easy.h" +#include +#include + +using namespace http; + +HttpClient::HttpClient(uv_loop_t *loop): m_eventLoop(loop) { + m_curlMulti = curl_multi_init(); + curl_multi_setopt(m_curlMulti , CURLMOPT_SOCKETFUNCTION, &HttpClient::curl_socket_cb); + curl_multi_setopt(m_curlMulti, CURLMOPT_SOCKETDATA, this); + curl_multi_setopt(m_curlMulti, CURLMOPT_TIMERFUNCTION, &HttpClient::curl_timer_cb); + curl_multi_setopt(m_curlMulti, CURLMOPT_TIMERDATA, this); + m_curlTimer = new uv_timer_t; + uv_timer_init(loop, m_curlTimer); + m_curlTimer->data = this; + m_logger = spdlog::get("httpclient"); + if (!m_logger) { + m_logger = spdlog::stdout_color_mt("httpclient"); + m_logger->set_level(spdlog::level::debug); + } +} + +HttpClient::~HttpClient() { + uv_close((uv_handle_t*)m_curlTimer, [](uv_handle_t *h){ + delete h; + }); + spdlog::warn("freeing curl structures is not yet implemented!"); +} + +bool HttpClient::send_request(std::string method, std::string url, HttpOptions opts, ResponseCallback cb) { + m_logger->debug("send request {} {}", method, url); + CURL *requestHandle = curl_easy_init(); + std::pair insertResult = m_requests.emplace(requestHandle, this); + if (!insertResult.second) { + curl_easy_cleanup(requestHandle); + return false; + } + auto requestData = insertResult.first; + requestData->second.callback = cb; + requestData->second.response = std::make_unique(); + curl_easy_setopt(requestHandle, CURLOPT_WRITEFUNCTION, &HttpClient::curl_data_cb); + curl_easy_setopt(requestHandle, CURLOPT_WRITEDATA, requestHandle); + curl_easy_setopt(requestHandle, CURLOPT_PRIVATE, this); + curl_easy_setopt(requestHandle, CURLOPT_FOLLOWLOCATION, 1); + + curl_easy_setopt(requestHandle, CURLOPT_URL, url.c_str()); + curl_easy_setopt(requestHandle, CURLOPT_CUSTOMREQUEST, method.c_str()); + + if (opts.headers) { + curl_slist *headerList = nullptr; + auto inputHeaderList = *opts.headers; + for (auto i = inputHeaderList.cbegin(); i != inputHeaderList.cend(); ++i) { + std::string headerLine; + headerLine.reserve(i->first.size() + 1 + i->second.size()); + headerLine += i->first; + headerLine += ':'; + headerLine += i->second; + curl_slist_append(headerList, headerLine.c_str()); + } + curl_easy_setopt(requestHandle, CURLOPT_HTTPHEADER, headerList); + requestData->second.requestHeaders = headerList; + } + + if (opts.body) { + curl_easy_setopt(requestHandle, CURLOPT_POSTFIELDS, opts.body->c_str()); + } + return CURLM_OK == curl_multi_add_handle(m_curlMulti, requestHandle); +} + +int HttpClient::curl_socket_cb(CURL *curl, curl_socket_t curlSocket, int action, HttpClient *self, void *socketPtr) { + (void)curl; + int pollFlags = 0; + CurlSocketData_ *data; + switch (action) { + case CURL_POLL_IN: + case CURL_POLL_INOUT: + case CURL_POLL_OUT: + self->m_logger->debug("polling socket {}", curlSocket); + if (!socketPtr) { + data = new CurlSocketData_; + data->curlSocket = curlSocket; + data->client = self; + data->pollHandle = new uv_poll_t; + uv_poll_init(self->m_eventLoop, data->pollHandle, curlSocket); + data->pollHandle->data = data; + } else { + data = reinterpret_cast(socketPtr); + } + curl_multi_assign(self->m_curlMulti, curlSocket, data); + if (action != CURL_POLL_OUT) pollFlags |= UV_READABLE; + if (action != CURL_POLL_IN) pollFlags |= UV_WRITABLE; + uv_poll_start(data->pollHandle, pollFlags, &HttpClient::uv_socket_cb); + break; + case CURL_POLL_REMOVE: + if (socketPtr) { + self->m_logger->debug("removing socket {}", curlSocket); + data = reinterpret_cast(socketPtr); + uv_poll_stop(data->pollHandle); + curl_multi_assign(self->m_curlMulti, curlSocket, nullptr); + data->pollHandle->data = nullptr; + uv_close((uv_handle_t*)data->pollHandle, [](uv_handle_t *h){ delete h; }); + delete data; + } + } + return 0; +} + +int HttpClient::curl_timer_cb(CURLM *curl, long timeout, HttpClient *self) { + if (timeout < 0) + uv_timer_stop(self->m_curlTimer); + else { + if (timeout == 0) timeout = 1; + uv_timer_start(self->m_curlTimer, &HttpClient::uv_timeout_cb, timeout, 0); + } + return 0; +} + +void HttpClient::uv_socket_cb(uv_poll_t *h, int status, int events) { + (void)status; + auto data = reinterpret_cast(h->data); + HttpClient *client = data->client; + if (!data) return; + data->client->m_logger->debug("socket {}", events & UV_READABLE ? (events & UV_WRITABLE ? "readable and writable" : "readable") : "writable"); + int flags = 0; + if (events & UV_READABLE) flags |= CURL_CSELECT_IN; + if (events & UV_WRITABLE) flags |= CURL_CSELECT_OUT; + int runningHandles; + curl_multi_socket_action(data->client->m_curlMulti, data->curlSocket, flags, &runningHandles); + client->check_curl_messages(); +} + +void HttpClient::uv_timeout_cb(uv_timer_t *h) { + auto self = reinterpret_cast(h->data); + self->m_logger->debug("curl timeout"); + if (self) { + int runningHandles; + curl_multi_socket_action(self->m_curlMulti, CURL_SOCKET_TIMEOUT, 0, &runningHandles); + self->check_curl_messages(); + } +} + +void HttpClient::check_curl_messages() { + CURLMsg *msg; + int pending; + while ((msg = curl_multi_info_read(m_curlMulti, &pending))) { + switch (msg->msg) { + case CURLMSG_DONE: { + CURLcode r = msg->data.result; + auto &request = m_requests.at(msg->easy_handle); + if (r == CURLE_OK) { + m_logger->debug("curl transfer done"); + long statusCode = 0; + curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &statusCode); + request.response->status = statusCode; + request.callback(std::move(request.response), CURLE_OK); + } else { + m_logger->error("curl transfer error: {}", (int)r); + request.callback(nullptr, r); + } + curl_multi_remove_handle(m_curlMulti, msg->easy_handle); + curl_easy_cleanup(msg->easy_handle); + if (request.requestHeaders) + curl_slist_free_all(request.requestHeaders); + m_requests.erase(msg->easy_handle); + break; + } + default: + break; + } + } +} + +size_t HttpClient::curl_data_cb(char *ptr, size_t size, size_t nmemb, CURL *userdata) { + HttpClient *self; + curl_easy_getinfo(userdata, CURLINFO_PRIVATE, &self); + HttpRequestData_ &req = self->m_requests.at(userdata); + self->m_logger->debug("received {} bytes", nmemb); + req.response->body.append(ptr, nmemb); + return nmemb; +} \ No newline at end of file diff --git a/http.h b/http.h new file mode 100644 index 0000000..374547c --- /dev/null +++ b/http.h @@ -0,0 +1,62 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +namespace http { + struct HttpResponse { + int status; + std::string body; + }; + + struct HttpRequestData_; + struct HttpOptions; + typedef std::function, CURLcode)> ResponseCallback; + + class HttpClient { + public: + HttpClient(uv_loop_t *loop); + HttpClient(HttpClient&&) = delete; + ~HttpClient(); + bool send_request(std::string method, std::string url, HttpOptions opts, ResponseCallback cb); + private: + void check_curl_messages(); + static int curl_socket_cb(CURL *curl, curl_socket_t curlSocket, int action, HttpClient *self, void *socketPtr); + static int curl_timer_cb(CURLM *curl, long timeout, HttpClient *self); + static size_t curl_data_cb(char *ptr, size_t size, size_t nmemb, CURL *userdata); + static void uv_socket_cb(uv_poll_t *h, int status, int events); + static void uv_timeout_cb(uv_timer_t *h); + + uv_loop_t *m_eventLoop; + uv_timer_t *m_curlTimer; + CURLM *m_curlMulti; + std::shared_ptr m_logger; + std::map m_requests; + }; + + struct CurlSocketData_ { + HttpClient *client; + curl_socket_t curlSocket; + uv_poll_t *pollHandle; + }; + + struct HttpRequestData_ { + HttpClient *client; + curl_slist *requestHeaders = nullptr; + ResponseCallback callback; + std::unique_ptr response; + HttpRequestData_(HttpClient *client) { + this->client = client; + } + }; + + struct HttpOptions { + std::optional>> headers; + std::optional body; + }; +} diff --git a/libuv b/libuv new file mode 160000 index 0000000..d4ab6fb --- /dev/null +++ b/libuv @@ -0,0 +1 @@ +Subproject commit d4ab6fbba4669935a6bc23645372dfe4ac29ab39 diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..48cb950 --- /dev/null +++ b/main.cpp @@ -0,0 +1,21 @@ +#include "http.h" +#include +#include +#include + +int main() { + uv_loop_t *loop = uv_default_loop(); + + http::HttpClient httpClient(loop); + spdlog::info("sending request"); + httpClient.send_request("GET", "https://slavasil.ru/", {}, [](auto resp, CURLcode code){ + if (code == 0) { + spdlog::info("got response! {} {}", resp->status, resp->body); + } else { + spdlog::error("got error!"); + } + }); + + uv_run(loop, UV_RUN_DEFAULT); + return 0; +} diff --git a/spdlog b/spdlog new file mode 160000 index 0000000..51a0dec --- /dev/null +++ b/spdlog @@ -0,0 +1 @@ +Subproject commit 51a0deca2c825f1d4461655a18bb37d6df76646d diff --git a/td b/td new file mode 160000 index 0000000..18618ca --- /dev/null +++ b/td @@ -0,0 +1 @@ +Subproject commit 18618cad563bf65848b64375b295421e014d4ae7