From d29c8dfcc9f379623a155e46655a328b2aa0efcf Mon Sep 17 00:00:00 2001 From: Slavasil Date: Thu, 14 Nov 2024 16:48:24 +0300 Subject: [PATCH] add basic HTTP client and its testing in main() --- .gitignore | 4 ++ .gitmodules | 12 ++++ CMakeLists.txt | 11 +++ curl | 1 + http.cpp | 182 +++++++++++++++++++++++++++++++++++++++++++++++++ http.h | 62 +++++++++++++++++ libuv | 1 + main.cpp | 21 ++++++ spdlog | 1 + td | 1 + 10 files changed, 296 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 160000 curl create mode 100644 http.cpp create mode 100644 http.h create mode 160000 libuv create mode 100644 main.cpp create mode 160000 spdlog create mode 160000 td 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