diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index f43254bd3..0d7670934 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -48,11 +48,11 @@ jobs: - name: Run tests run: ctest --test-dir build - name: Run engine tests - timeout-minutes: 1 + timeout-minutes: 5 run: | chmod +x build/VoxelEngine chmod +x AppDir/usr/bin/vctest - AppDir/usr/bin/vctest -e build/VoxelEngine -d dev/tests -u build + AppDir/usr/bin/vctest -e build/VoxelEngine -d dev/tests -u build --memchecker valgrind - name: Build AppImage uses: AppImageCrafters/build-appimage-action@fe2205a4d6056be47051f7b1b3811106e9814910 env: diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index c3f5156f9..b4d9f6328 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -23,13 +23,13 @@ jobs: - name: Install dependencies from brew run: | - brew install glfw3 glew libpng openal-soft luajit libvorbis skypjack/entt/entt googletest glm freetype + brew install glfw3 glew libpng openal-soft luajit libvorbis skypjack/entt/entt googletest glm freetype - name: Configure run: | cmake -DOPENAL_INCLUDE_DIR=$(brew --prefix openal-soft)/include \ -DOPENAL_LIBRARY=$(brew --prefix openal-soft)/lib/libopenal.dylib \ - -S . -B build -DCMAKE_BUILD_TYPE=Release -DVOXELENGINE_BUILD_TESTS=ON -DVOXELENGINE_BUILD_APPDIR=1 -DVC_BUILD_NAME="${{ inputs.build_name }}" + -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVOXELENGINE_BUILD_TESTS=ON -DVOXELENGINE_BUILD_APPDIR=1 -DVC_BUILD_NAME="${{ inputs.build_name }}" - name: Build run: | @@ -45,11 +45,11 @@ jobs: run: ctest --output-on-failure --test-dir build - name: Run engine tests - timeout-minutes: 1 + timeout-minutes: 5 run: | chmod +x build/VoxelEngine chmod +x AppDir/usr/bin/vctest - AppDir/usr/bin/vctest -e build/VoxelEngine -d dev/tests -u build --output-always + AppDir/usr/bin/vctest -e build/VoxelEngine -d dev/tests -u build --output-always --debugger "lldb -batch -o 'run' -o 'thread backtrace all' -o 'quit' -- " - name: Create DMG run: | mkdir VoxelEngineDmgContent diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index f248b6ab6..ffcb0c318 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -38,7 +38,7 @@ jobs: - name: Run engine tests run: | build/vctest/RelWithDebInfo/vctest.exe -e build/RelWithDebInfo/VoxelEngine.exe -d dev/tests -u build - timeout-minutes: 1 + timeout-minutes: 5 - name: Package for Windows run: | mkdir packaged diff --git a/dev/tests/network_http.lua b/dev/tests/network_http.lua index 5861460de..12149de09 100644 --- a/dev/tests/network_http.lua +++ b/dev/tests/network_http.lua @@ -8,4 +8,4 @@ end, function (code) response_received = true end) -app.sleep_until(function () return response_received end, nil, 10) +app.sleep_until(function () return response_received end, nil, 5) diff --git a/doc/en/scripting/builtins/libnetwork.md b/doc/en/scripting/builtins/libnetwork.md index 0ce5c1b0e..8893407d4 100644 --- a/doc/en/scripting/builtins/libnetwork.md +++ b/doc/en/scripting/builtins/libnetwork.md @@ -4,6 +4,39 @@ A library for working with the network. ## HTTP requests +There is a configurable function `network.request` that allows performing HTTP requests with various methods (GET, POST, PUT, DELETE, etc.) and configuring headers, request body, timeout, and other parameters. + +> To pass binary data in the request body, use a byte array (Bytearray) or a string. In `on_response`, the `body` string can be converted to a byte array using `Bytearray(response.body)`. + +```lua +network.request( + url: string, + parameters: { + -- Request method (GET, POST, PUT, DELETE, etc.) + method: string, + -- Request body as a string or Bytearray + body: table|string, + -- List of additional request headers + headers: table, + -- Timeout in milliseconds + timeout: int, + -- Whether to verify the SSL certificate + verify_ssl: boolean, + -- Function called when a response is received + on_response: function(response: { + -- HTTP response status code + status: int, + -- Response body as a string + body: string, + -- List of response headers + headers: table + }), + } +) +``` + +### Simplified functions for GET and POST requests + ```lua -- Performs a GET request to the specified URL. network.get( diff --git a/doc/ru/scripting/builtins/libnetwork.md b/doc/ru/scripting/builtins/libnetwork.md index 98c4e51e0..c60f0f178 100644 --- a/doc/ru/scripting/builtins/libnetwork.md +++ b/doc/ru/scripting/builtins/libnetwork.md @@ -4,6 +4,39 @@ ## HTTP-Запросы +Существует настраиваемая функция `network.request`, которая позволяет выполнять HTTP-запросы с различными методами (GET, POST, PUT, DELETE и т.д.) и настраивать заголовки, тело запроса, таймаут и другие параметры. + +> Для передачи двоичных данных в теле запроса, используйте массив байт (Bytearray) или строку. В `on_response` строку `body` можно преобразовать в массив байт с помощью `Bytearray(response.body)`. + +```lua +network.request( + url: string, + parameters: { + -- Метод запроса (GET, POST, PUT, DELETE и т.д.) + method: string, + -- Тело запроса в виде строки или массива байт (Bytearray) + body: table|string, + -- Список дополнительных заголовков запроса + headers: table, + -- Таймаут в миллисекундах + timeout: int, + -- Проверять ли SSL-сертификат + verify_ssl: boolean, + -- Функция, вызываемая при получении ответа + on_response: function(response: { + -- HTTP-код ответа + status: int, + -- Тело ответа в виде строки + body: string, + -- Список заголовков ответа + headers: table + }), + } +) +``` + +### Упрощённые функции для GET и POST запросов + ```lua -- Выполняет GET запрос к указанному URL. network.get( @@ -231,4 +264,4 @@ network.get_total_download() -> int ```lua -- Ищет свободный для использования порт. network.find_free_port() -> int | nil -``` \ No newline at end of file +``` diff --git a/res/scripts/classes.lua b/res/scripts/classes.lua index 280c5ddeb..4a8c8915b 100644 --- a/res/scripts/classes.lua +++ b/res/scripts/classes.lua @@ -98,40 +98,80 @@ local _udp_server_callbacks = {} local _udp_client_datagram_callbacks = {} local _udp_client_open_callbacks = {} local _http_response_callbacks = {} -local _http_error_callbacks = {} + +local http_request = network.__request +local open_tcp = network.__open_tcp +local open_udp = network.__open_udp +local connect_tcp = network.__connect_tcp +local connect_udp = network.__connect_udp +network.__request = nil +network.__open_tcp = nil +network.__open_udp = nil +network.__connect_tcp = nil +network.__connect_udp = nil + +local function request(url, params) + local id = http_request(url, params) + if params.on_response then + _http_response_callbacks[id] = params.on_response + end +end + +network.request = request network.get = function(url, callback, errorCallback, headers) - local id = network.__get(url, headers) - if callback then - _http_response_callbacks[id] = callback - end - if errorCallback then - _http_error_callbacks[id] = errorCallback - end + return request(url, { + method = "GET", + headers = headers, + on_response = function(response) + if response.status / 100 == 2 then + return callback(response.body) + else + return errorCallback(response.status, response.body) + end + end, + on_error = function(response) + errorCallback(response.status, response.body) + end, + follow_location = true, + }) end network.get_binary = function(url, callback, errorCallback, headers) - local id = network.__get_binary(url, headers) - if callback then - _http_response_callbacks[id] = callback - end - if errorCallback then - _http_error_callbacks[id] = errorCallback - end + return request(url, { + method = "GET", + headers = headers, + on_response = callback and (function (response) + if response.code / 100 == 2 then + return callback(Bytearray(response.body)) + else + return errorCallback(response.status, response.body) + end + end), + follow_location = true, + }) end -network.post = function(url, data, callback, errorCallback, headers) - local id = network.__post(url, data, headers) - if callback then - _http_response_callbacks[id] = callback - end - if errorCallback then - _http_error_callbacks[id] = errorCallback - end +network.post = function(url, body, callback, errorCallback, headers) + return request(url, { + method = "POST", + headers = table.extend({ + "Content-Type: application/json" + }, headers), + body = body, + on_response = function(response) + if response.code / 100 == 2 then + return callback(Bytearray(response.body)) + else + return errorCallback(response.status, response.body) + end + end, + follow_location = true, + }) end network.tcp_open = function (port, handler) - local socket = setmetatable({id=network.__open_tcp(port)}, ServerSocket) + local socket = setmetatable({id=open_tcp(port)}, ServerSocket) _tcp_server_callbacks[socket.id] = function(id) handler(setmetatable({id=id}, Socket)) @@ -141,7 +181,7 @@ end network.tcp_connect = function(address, port, callback, errorCallback) local socket = setmetatable({id=0}, Socket) - socket.id = network.__connect_tcp(address, port) + socket.id = connect_tcp(address, port) _tcp_client_callbacks[socket.id] = function() callback(socket) end if errorCallback then _tcp_client_error_callbacks[socket.id] = function(message) errorCallback(socket, message) end @@ -154,7 +194,7 @@ network.udp_open = function (port, datagramHandler) error "udp server cannot be opened without datagram handler" end - local socket = setmetatable({id=network.__open_udp(port)}, DatagramServerSocket) + local socket = setmetatable({id=open_udp(port)}, DatagramServerSocket) _udp_server_callbacks[socket.id] = function(address, port, data) datagramHandler(address, port, data, socket) @@ -169,7 +209,7 @@ network.udp_connect = function (address, port, datagramHandler, openCallback) end local socket = setmetatable({id=0}, WriteableSocket) - socket.id = network.__connect_udp(address, port) + socket.id = connect_udp(address, port) _udp_client_datagram_callbacks[socket.id] = datagramHandler if openCallback then @@ -239,20 +279,10 @@ network.__process_events = function() end end elseif etype == RESPONSE then - if event[2] / 100 == 2 then - local callback = _http_response_callbacks[event[3]] - _http_response_callbacks[event[3]] = nil - _http_error_callbacks[event[3]] = nil - if callback then - callback(event[4]) - end - else - local callback = _http_error_callbacks[event[3]] - _http_response_callbacks[event[3]] = nil - _http_error_callbacks[event[3]] = nil - if callback then - callback(event[2], event[4]) - end + local callback = _http_response_callbacks[event[3]] + _http_response_callbacks[event[3]] = nil + if callback then + callback(event[4]) end end diff --git a/src/logic/scripting/lua/libs/libnetwork.cpp b/src/logic/scripting/lua/libs/libnetwork.cpp index 55db6e550..59d50f075 100644 --- a/src/logic/scripting/lua/libs/libnetwork.cpp +++ b/src/logic/scripting/lua/libs/libnetwork.cpp @@ -3,6 +3,7 @@ #include "engine/Engine.hpp" #include "network/Network.hpp" #include "devtools/Project.hpp" +#include "util/stringutil.hpp" #include #include @@ -27,7 +28,7 @@ struct ResponseEventDto { int status; bool binary; int requestId; - std::vector bytes; + network::HttpResponse response; }; enum NetworkDatagramSide { @@ -85,74 +86,55 @@ static std::vector read_headers(lua::State* L, int index) { static int request_id = 1; -static int perform_get(lua::State* L, network::Network& network, bool binary) { - std::string url(lua::require_lstring(L, 1)); - auto headers = read_headers(L, 2); +static int l_request(lua::State* L, network::Network& network) { + network::HttpRequest request {}; + request.url = lua::require_lstring(L, 1); - int currentRequestId = request_id++; - - network.get( - url, - [currentRequestId, binary](std::vector bytes) { - push_event(NetworkEvent( - RESPONSE, - ResponseEventDto { - 200, binary, currentRequestId, std::move(bytes)} - )); - }, - [currentRequestId, binary](int code, std::vector bytes) { - push_event(NetworkEvent( - RESPONSE, - ResponseEventDto { - code, binary, currentRequestId, std::move(bytes)} - )); - }, - std::move(headers) - ); - return lua::pushinteger(L, currentRequestId); -} - -static int l_get(lua::State* L, network::Network& network) { - return perform_get(L, network, false); -} - -static int l_get_binary(lua::State* L, network::Network& network) { - return perform_get(L, network, true); -} - -static int l_post(lua::State* L, network::Network& network) { - std::string url(lua::require_lstring(L, 1)); - auto data = lua::tovalue(L, 2); - - std::string string; - if (data.isString()) { - string = data.asString(); - } else { - string = json::stringify(data, false); + if (!lua::istable(L, 2)) { + throw std::runtime_error("table expected as argument #2"); + } + if (lua::getfield(L, "method", 2)) { + request.method = lua::require_string(L, -1); + lua::pop(L); + } + if (lua::getfield(L, "headers", 2)) { + request.headers = read_headers(L, -1); + lua::pop(L); + } + if (lua::getfield(L, "body", 2)) { + if (lua::type(L, -1) == LUA_TCDATA) { + request.body = lua::bytearray_as_string(L, -1); + } else { + request.body = lua::require_lstring(L, -1); + } + lua::pop(L); + } + if (lua::getfield(L, "follow_location", 2)) { + request.followLocation = lua::toboolean(L, -1); + lua::pop(L); + } + if (lua::getfield(L, "timeout_ms", 2)) { + request.timeoutMs = lua::tointeger(L, -1); + lua::pop(L); + } + if (lua::getfield(L, "verify_ssl", 2)) { + request.verifySSL = lua::toboolean(L, -1); + lua::pop(L); } - auto headers = read_headers(L, 3); int currentRequestId = request_id++; + request.onResponse = [currentRequestId](network::HttpResponse response) { + push_event(NetworkEvent( + RESPONSE, + ResponseEventDto { + response.status, + false, + currentRequestId, + std::move(response)} + )); + }; - network.post( - url, - string, - [currentRequestId](std::vector bytes) { - push_event(NetworkEvent( - RESPONSE, - ResponseEventDto { - 200, false, currentRequestId, std::move(bytes)} - )); - }, - [currentRequestId](int code, std::vector bytes) { - push_event(NetworkEvent( - RESPONSE, - ResponseEventDto { - code, false, currentRequestId, std::move(bytes)} - )); - }, - std::move(headers) - ); + network.request(std::move(request)); return lua::pushinteger(L, currentRequestId); } @@ -519,11 +501,31 @@ static int l_pull_events(lua::State* L) { lua::pushinteger(L, dto.requestId); lua::rawseti(L, 3); + lua::createtable(L, 0, 2); + lua::pushinteger(L, dto.response.status); + lua::setfield(L, "status"); + if (dto.binary) { - lua::create_bytearray(L, dto.bytes.data(), dto.bytes.size()); + lua::create_bytearray( + L, dto.response.body.data(), dto.response.body.size() + ); } else { - lua::pushlstring(L, std::string_view(dto.bytes.data(), dto.bytes.size())); + lua::pushlstring( + L, + std::string_view( + dto.response.body.data(), dto.response.body.size() + ) + ); } + lua::setfield(L, "body"); + + lua::createtable(L, dto.response.headers.size(), 0); + for (int i = 0; i < dto.response.headers.size(); i++) { + lua::pushlstring(L, dto.response.headers[i]); + lua::rawseti(L, i + 1); + } + lua::setfield(L, "headers"); + lua::rawseti(L, 4); break; } @@ -562,9 +564,7 @@ int wrap(lua_State* L) { } const luaL_Reg networklib[] = { - {"__get", wrap}, - {"__get_binary", wrap}, - {"__post", wrap}, + {"__request", wrap}, {"get_total_upload", wrap}, {"get_total_download", wrap}, {"find_free_port", wrap}, diff --git a/src/network/Curl.cpp b/src/network/Curl.cpp index 1caafde73..1c6507d36 100644 --- a/src/network/Curl.cpp +++ b/src/network/Curl.cpp @@ -4,7 +4,6 @@ #define NOMINMAX #include -#include using namespace network; @@ -23,97 +22,96 @@ static size_t write_callback( return size * nmemb; } -enum class RequestType { - GET, POST -}; +static size_t header_callback( + char* buffer, size_t size, size_t nitems, void* userdata +) { + auto* headers = static_cast*>(userdata); + size_t len = size * nitems; + std::string header(buffer, len); -struct Request { - RequestType type; - std::string url; - OnResponse onResponse; - OnReject onReject; - long maxSize; - bool followLocation = false; - std::string data; + while (!header.empty() && + (header.back() == '\r' || header.back() == '\n')) { + header.pop_back(); + } + + headers->push_back(std::move(header)); + return len; +} + +struct ProcessingRequest { + CURLM* multiHandle; + CURL* curl; + HttpRequest request; + std::vector buffer; std::vector headers; + + ProcessingRequest(CURLM* multiHandle) : multiHandle(multiHandle) { + curl = curl_easy_init(); + } + + ProcessingRequest(const ProcessingRequest&) = delete; + ProcessingRequest& operator=(const ProcessingRequest&) = delete; + + ProcessingRequest(ProcessingRequest&& other) noexcept + : multiHandle(other.multiHandle), + curl(other.curl), + request(std::move(other.request)), + buffer(std::move(other.buffer)), + headers(std::move(other.headers)) { + other.curl = nullptr; + other.multiHandle = nullptr; + } + + ProcessingRequest& operator=(ProcessingRequest&& other) noexcept { + if (this != &other) { + if (curl) { + curl_multi_remove_handle(multiHandle, curl); + curl_easy_cleanup(curl); + } + multiHandle = other.multiHandle; + curl = other.curl; + request = std::move(other.request); + buffer = std::move(other.buffer); + headers = std::move(other.headers); + other.curl = nullptr; + other.multiHandle = nullptr; + } + return *this; + } + + ~ProcessingRequest() { + if (curl) { + curl_multi_remove_handle(multiHandle, curl); + curl_easy_cleanup(curl); + } + } }; class CurlRequests : public Requests { CURLM* multiHandle; - CURL* curl; + std::vector> requests; size_t totalUpload = 0; size_t totalDownload = 0; - - OnResponse onResponse; - OnReject onReject; - std::vector buffer; - std::string url; - - std::queue requests; public: - CurlRequests(CURLM* multiHandle, CURL* curl) - : multiHandle(multiHandle), curl(curl) { + CurlRequests(CURLM* multiHandle) : multiHandle(multiHandle) { } virtual ~CurlRequests() { - curl_multi_remove_handle(multiHandle, curl); - curl_easy_cleanup(curl); + requests.clear(); curl_multi_cleanup(multiHandle); } - void get( - const std::string& url, - OnResponse onResponse, - OnReject onReject, - std::vector headers, - long maxSize - ) override { - Request request { - RequestType::GET, - url, - onResponse, - onReject, - maxSize, - true, - "", - std::move(headers)}; + + void request(HttpRequest request) override { processRequest(std::move(request)); } - void post( - const std::string& url, - const std::string& data, - OnResponse onResponse, - OnReject onReject=nullptr, - std::vector headers = {}, - long maxSize=0 - ) override { - Request request { - RequestType::POST, - url, - onResponse, - onReject, - maxSize, - false, - "", - std::move(headers)}; - request.data = data; - processRequest(std::move(request)); - } + void processRequest(HttpRequest request) { + auto entry = std::make_unique(multiHandle); + auto curl = entry->curl; - void processRequest(Request request) { - if (!url.empty()) { - requests.push(request); - return; - } - onResponse = request.onResponse; - onReject = request.onReject; - url = request.url; - - buffer.clear(); - - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_POST, request.type == RequestType::POST); + curl_easy_setopt(curl, CURLOPT_URL, request.url.c_str()); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, request.method.c_str()); curl_slist* hs = nullptr; @@ -121,22 +119,25 @@ public: hs = curl_slist_append(hs, header.c_str()); } - switch (request.type) { - case RequestType::GET: - break; - case RequestType::POST: - hs = curl_slist_append(hs, "Content-Type: application/json"); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, request.data.length()); - curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, request.data.c_str()); - break; - default: - throw std::runtime_error("not implemented"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(request.body.length())); + if (!request.body.empty()) { + curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, request.body.data()); } + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, static_cast(request.verifySSL)); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, static_cast(request.verifySSL)); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hs); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, request.followLocation); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, static_cast(request.followLocation)); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &entry->buffer); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &entry->headers); curl_easy_setopt(curl, CURLOPT_USERAGENT, "curl/7.81.0"); + if (request.timeoutMs > 0) { + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, request.timeoutMs); + } +#ifndef NDEBUG + // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); +#endif if (request.maxSize == 0) { curl_easy_setopt( curl, CURLOPT_MAXFILESIZE, std::numeric_limits::max() @@ -144,69 +145,105 @@ public: } else { curl_easy_setopt(curl, CURLOPT_MAXFILESIZE, request.maxSize); } - curl_multi_add_handle(multiHandle, curl); - int running; - CURLMcode res = curl_multi_perform(multiHandle, &running); - if (res != CURLM_OK) { - auto message = curl_multi_strerror(res); - logger.error() << message << " (" << url << ")"; - if (onReject) { - onReject(HTTP_BAD_GATEWAY, {}); + { + CURLMcode res = curl_multi_add_handle(multiHandle, curl); + if (res != CURLM_OK) { + auto message = curl_multi_strerror(res); + logger.error() << message << " (" << request.url << ")"; + if (request.onResponse) { + request.onResponse({0, {}, {}}); + } + return; } - url = ""; } + { + int running; + CURLMcode res = curl_multi_perform(multiHandle, &running); + if (res != CURLM_OK) { + auto message = curl_multi_strerror(res); + logger.error() << message << " (" << request.url << ")"; + if (request.onResponse) { + request.onResponse({0, {}, {}}); + } + return; + } + } + entry->request = std::move(request); + requests.push_back(std::move(entry)); } void update() override { - int messagesLeft; - int running; - CURLMsg* msg; - CURLMcode res = curl_multi_perform(multiHandle, &running); - if (res != CURLM_OK) { + if (CURLMcode res = curl_multi_poll( + multiHandle, + nullptr, + 0, + 0, + nullptr + )) { auto message = curl_multi_strerror(res); - logger.error() << message << " (" << url << ")"; - if (onReject) { - onReject(HTTP_BAD_GATEWAY, {}); - } - curl_multi_remove_handle(multiHandle, curl); - url = ""; + logger.error() << message; return; } - if ((msg = curl_multi_info_read(multiHandle, &messagesLeft)) != nullptr) { - if(msg->msg == CURLMSG_DONE) { - curl_multi_remove_handle(multiHandle, curl); - } - int response; - curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &response); - if (response == HTTP_OK) { - long size; - if (!curl_easy_getinfo(curl, CURLINFO_REQUEST_SIZE, &size)) { - totalUpload += size; - } - if (!curl_easy_getinfo(curl, CURLINFO_HEADER_SIZE, &size)) { - totalDownload += size; - } - totalDownload += buffer.size(); - if (onResponse) { - onResponse(std::move(buffer)); - } - } else { - logger.error() - << "response code " << response << " (" << url << ")" - << (buffer.empty() - ? "" - : std::to_string(buffer.size()) + " byte(s)"); - totalDownload += buffer.size(); - if (onReject) { - onReject(response, std::move(buffer)); - } - } - url = ""; + + int messagesLeft; + int running; + if (CURLMcode res = curl_multi_perform(multiHandle, &running)) { + auto message = curl_multi_strerror(res); + logger.error() << message; + return; } - if (url.empty() && !requests.empty()) { - auto request = std::move(requests.front()); - requests.pop(); - processRequest(std::move(request)); + CURLMsg* msg = curl_multi_info_read(multiHandle, &messagesLeft); + if (msg == nullptr || msg->msg != CURLMSG_DONE) { + return; + } + auto curl = msg->easy_handle; + auto found = std::find_if( + requests.begin(), + requests.end(), + [curl](const std::unique_ptr& entry) { + return entry && entry->curl == curl; + } + ); + if (requests.empty() || found == requests.end()) { + logger.error() << "could not find request for cURL handle"; + return; + } + auto entry = std::move(*found); + auto& req = entry->request; + requests.erase(found); + + long response = -1; + CURLcode result = msg->data.result; + curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &response); + auto headers = std::move(entry->headers); + if (response == 0) { + auto message = std::string(curl_easy_strerror(result)); + logger.error() << message << " (" << req.url << ")"; + if (req.onResponse) { + req.onResponse( + {static_cast(response), + std::move(headers), + std::vector( + message.data(), message.data() + message.size() + )} + ); + } + } else { + long size; + if (!curl_easy_getinfo(curl, CURLINFO_REQUEST_SIZE, &size)) { + totalUpload += size; + } + if (!curl_easy_getinfo(curl, CURLINFO_HEADER_SIZE, &size)) { + totalDownload += size; + } + totalDownload += entry->buffer.size(); + if (req.onResponse) { + req.onResponse({ + static_cast(response), + std::move(headers), + std::move(entry->buffer), + }); + } } } @@ -219,16 +256,11 @@ public: } static std::unique_ptr create() { - auto curl = curl_easy_init(); - if (curl == nullptr) { - throw std::runtime_error("could not initialzie cURL"); - } auto multiHandle = curl_multi_init(); if (multiHandle == nullptr) { - curl_easy_cleanup(curl); - throw std::runtime_error("could not initialzie cURL-multi"); + throw std::runtime_error("could not initialize cURL-multi"); } - return std::make_unique(multiHandle, curl); + return std::make_unique(multiHandle); } }; diff --git a/src/network/Network.cpp b/src/network/Network.cpp index a0e1eb606..7b526a29a 100644 --- a/src/network/Network.cpp +++ b/src/network/Network.cpp @@ -51,27 +51,8 @@ Network::Network(std::unique_ptr requests) Network::~Network() = default; -void Network::get( - const std::string& url, - OnResponse onResponse, - OnReject onReject, - std::vector headers, - long maxSize -) { - requests->get(url, onResponse, onReject, std::move(headers), maxSize); -} - -void Network::post( - const std::string& url, - const std::string& fieldsData, - OnResponse onResponse, - OnReject onReject, - std::vector headers, - long maxSize -) { - requests->post( - url, fieldsData, onResponse, onReject, std::move(headers), maxSize - ); +void Network::request(HttpRequest request) { + requests->request(std::move(request)); } Connection* Network::getConnection(u64id_t id, bool includePrivate) { diff --git a/src/network/Network.hpp b/src/network/Network.hpp index 6d5cd6d54..49f44af6c 100644 --- a/src/network/Network.hpp +++ b/src/network/Network.hpp @@ -68,22 +68,7 @@ namespace network { Network(std::unique_ptr requests); ~Network(); - void get( - const std::string& url, - OnResponse onResponse, - OnReject onReject = nullptr, - std::vector headers = {}, - long maxSize=0 - ); - - void post( - const std::string& url, - const std::string& fieldsData, - OnResponse onResponse, - OnReject onReject = nullptr, - std::vector headers = {}, - long maxSize=0 - ); + void request(HttpRequest request); [[nodiscard]] Connection* getConnection(u64id_t id, bool includePrivate); [[nodiscard]] Server* getServer(u64id_t id, bool includePrivate) const; diff --git a/src/network/commons.hpp b/src/network/commons.hpp index 8b834bb3e..cd98fbe00 100644 --- a/src/network/commons.hpp +++ b/src/network/commons.hpp @@ -10,33 +10,38 @@ #include namespace network { - using OnResponse = std::function)>; - using OnReject = std::function)>; + struct HttpResponse; + + using OnResponse = std::function; using ConnectCallback = std::function; using ConnectErrorCallback = std::function; using ServerDatagramCallback = std::function; using ClientDatagramCallback = std::function; + struct HttpRequest { + std::string method; + std::string url; + std::string body; + std::vector headers; + + OnResponse onResponse; + bool followLocation = false; + bool verifySSL = true; + long maxSize = -1; + long timeoutMs = 0; + }; + + struct HttpResponse { + int status; + std::vector headers; + std::vector body; + }; + class Requests { public: virtual ~Requests() {} - virtual void get( - const std::string& url, - OnResponse onResponse, - OnReject onReject=nullptr, - std::vector headers = {}, - long maxSize=0 - ) = 0; - - virtual void post( - const std::string& url, - const std::string& data, - OnResponse onResponse, - OnReject onReject=nullptr, - std::vector headers = {}, - long maxSize=0 - ) = 0; + virtual void request(HttpRequest request) = 0; [[nodiscard]] virtual size_t getTotalUpload() const = 0; [[nodiscard]] virtual size_t getTotalDownload() const = 0; diff --git a/src/util/stringutil.cpp b/src/util/stringutil.cpp index 55529a398..8d80a82ab 100644 --- a/src/util/stringutil.cpp +++ b/src/util/stringutil.cpp @@ -543,6 +543,14 @@ static std::basic_string upper_case(const std::basic_string& str) return result; } +std::string util::lower_case(const std::string& str) { + return ::lower_case(str); +} + +std::string util::upper_case(const std::string& str) { + return ::upper_case(str); +} + std::wstring util::lower_case(const std::wstring& str) { return ::lower_case(str); } diff --git a/src/util/stringutil.hpp b/src/util/stringutil.hpp index c26f02741..3f2263efb 100644 --- a/src/util/stringutil.hpp +++ b/src/util/stringutil.hpp @@ -101,6 +101,8 @@ namespace util { std::string capitalized(const std::string& str); + std::string lower_case(const std::string& str); + std::string upper_case(const std::string& str); std::wstring lower_case(const std::wstring& str); std::wstring upper_case(const std::wstring& str); std::wstring capitalized(const std::wstring& str); diff --git a/test/network/curltest.cpp b/test/network/curltest.cpp index 42806c77c..e7fc4840d 100644 --- a/test/network/curltest.cpp +++ b/test/network/curltest.cpp @@ -6,18 +6,21 @@ TEST(curltest, curltest) { NetworkSettings settings {}; auto network = network::Network::create(settings); - network->get( - "https://raw.githubusercontent.com/MihailRis/VoxelEngine-Cpp/refs/" - "heads/curl/res/content/base/blocks/lamp.json", - [](std::vector data) { - if (data.empty()) { - return; - } - auto view = std::string_view(data.data(), data.size()); - auto value = json::parse(view); - std::cout << value << std::endl; - }, [](auto, auto){} - ); + + network::HttpRequest request {}; + request.url = "https://raw.githubusercontent.com/MihailRis/VoxelEngine-Cpp/refs/" + "heads/curl/res/content/base/blocks/lamp.json"; + request.onResponse = [](network::HttpResponse response) { + if (response.body.empty()) { + return; + } + auto view = + std::string_view(response.body.data(), response.body.size()); + auto value = json::parse(view); + std::cout << value << std::endl; + }; + network->request(std::move(request)); + std::cout << "upload: " << network->getTotalUpload() << " B" << std::endl; std::cout << "download: " << network->getTotalDownload() << " B" << std::endl; } diff --git a/vctest/main.cpp b/vctest/main.cpp index ee2f15e5b..99eebaacd 100644 --- a/vctest/main.cpp +++ b/vctest/main.cpp @@ -16,7 +16,8 @@ struct Config { fs::path directory; fs::path resDir {"res"}; fs::path workingDir {"."}; - std::string memchecker = "valgrind"; + std::string memchecker = ""; + std::string debugger = ""; bool outputAlways = false; }; @@ -30,6 +31,7 @@ static bool perform_keyword( std::cout << " --tests , -d = tests directory path\n"; std::cout << " --res , -r = 'res' directory path\n"; std::cout << " --user , -u = user directory path\n"; + std::cout << " --debugger = debugger with run command\n"; std::cout << " --memchecker = path to valgrind\n"; std::cout << " --output-always = always show tests output\n"; std::cout << std::endl; @@ -44,6 +46,8 @@ static bool perform_keyword( config.workingDir = fs::path(reader.next()); } else if (keyword == "--output-always") { config.outputAlways = true; + } else if (keyword == "--debugger") { + config.debugger = reader.next(); } else if (keyword == "--memchecker") { config.memchecker = reader.next(); } else { @@ -172,7 +176,7 @@ static std::string fix_path(std::string s) { return s; } -static bool run_test(const Config& config, const fs::path& path, bool memcheck = false) { +static bool run_test(const Config& config, const fs::path& path, bool debugRun = false) { using std::chrono::duration_cast; using std::chrono::high_resolution_clock; using std::chrono::milliseconds; @@ -182,9 +186,16 @@ static bool run_test(const Config& config, const fs::path& path, bool memcheck = auto name = path.stem(); std::stringstream ss; - if (memcheck) { - ss << config.memchecker << " --log-file=" - << fix_path(memcheckLogFile.string()) << " "; + if (debugRun) { + if (!config.memchecker.empty()) { + ss << config.memchecker; + ss << " --log-file=" << fix_path(memcheckLogFile.string()); + ss << " "; + } + if (!config.debugger.empty()) { + ss << config.debugger; + ss << " "; + } } ss << fs::canonical(config.executable) << " --headless"; ss << " --test " << fix_path(path.string()); @@ -203,7 +214,7 @@ static bool run_test(const Config& config, const fs::path& path, bool memcheck = .count(); if (code) { - if (memcheck) { + if (debugRun) { // valgrind-specific output display_segfault_valgrind(memcheckLogFile, name, std::cerr); fs::remove(memcheckLogFile);