Merge pull request #972 from MihailRis/upgrade-http-in-vc

upgrade http requests
This commit is contained in:
MihailRis 2026-09-14 22:42:33 +03:00 • committed by GitHub
commit 85cc55c63b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 461 additions and 338 deletions

View file

@ -48,11 +48,11 @@ jobs:
- name: Run tests - name: Run tests
run: ctest --test-dir build run: ctest --test-dir build
- name: Run engine tests - name: Run engine tests
timeout-minutes: 1 timeout-minutes: 5
run: | run: |
chmod +x build/VoxelEngine chmod +x build/VoxelEngine
chmod +x AppDir/usr/bin/vctest 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 - name: Build AppImage
uses: AppImageCrafters/build-appimage-action@fe2205a4d6056be47051f7b1b3811106e9814910 uses: AppImageCrafters/build-appimage-action@fe2205a4d6056be47051f7b1b3811106e9814910
env: env:

View file

@ -23,13 +23,13 @@ jobs:
- name: Install dependencies from brew - name: Install dependencies from brew
run: | 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 - name: Configure
run: | run: |
cmake -DOPENAL_INCLUDE_DIR=$(brew --prefix openal-soft)/include \ cmake -DOPENAL_INCLUDE_DIR=$(brew --prefix openal-soft)/include \
-DOPENAL_LIBRARY=$(brew --prefix openal-soft)/lib/libopenal.dylib \ -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 - name: Build
run: | run: |
@ -45,11 +45,11 @@ jobs:
run: ctest --output-on-failure --test-dir build run: ctest --output-on-failure --test-dir build
- name: Run engine tests - name: Run engine tests
timeout-minutes: 1 timeout-minutes: 5
run: | run: |
chmod +x build/VoxelEngine chmod +x build/VoxelEngine
chmod +x AppDir/usr/bin/vctest 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 - name: Create DMG
run: | run: |
mkdir VoxelEngineDmgContent mkdir VoxelEngineDmgContent

View file

@ -38,7 +38,7 @@ jobs:
- name: Run engine tests - name: Run engine tests
run: | run: |
build/vctest/RelWithDebInfo/vctest.exe -e build/RelWithDebInfo/VoxelEngine.exe -d dev/tests -u build 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 - name: Package for Windows
run: | run: |
mkdir packaged mkdir packaged

View file

@ -8,4 +8,4 @@ end, function (code)
response_received = true response_received = true
end) end)
app.sleep_until(function () return response_received end, nil, 10) app.sleep_until(function () return response_received end, nil, 5)

View file

@ -4,6 +4,39 @@ A library for working with the network.
## HTTP requests ## 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<string>,
-- 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<string>
}),
}
)
```
### Simplified functions for GET and POST requests
```lua ```lua
-- Performs a GET request to the specified URL. -- Performs a GET request to the specified URL.
network.get( network.get(

View file

@ -4,6 +4,39 @@
## HTTP-Запросы ## 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<string>,
-- Таймаут в миллисекундах
timeout: int,
-- Проверять ли SSL-сертификат
verify_ssl: boolean,
-- Функция, вызываемая при получении ответа
on_response: function(response: {
-- HTTP-код ответа
status: int,
-- Тело ответа в виде строки
body: string,
-- Список заголовков ответа
headers: table<string>
}),
}
)
```
### Упрощённые функции для GET и POST запросов
```lua ```lua
-- Выполняет GET запрос к указанному URL. -- Выполняет GET запрос к указанному URL.
network.get( network.get(
@ -231,4 +264,4 @@ network.get_total_download() -> int
```lua ```lua
-- Ищет свободный для использования порт. -- Ищет свободный для использования порт.
network.find_free_port() -> int | nil network.find_free_port() -> int | nil
``` ```

View file

@ -98,40 +98,80 @@ local _udp_server_callbacks = {}
local _udp_client_datagram_callbacks = {} local _udp_client_datagram_callbacks = {}
local _udp_client_open_callbacks = {} local _udp_client_open_callbacks = {}
local _http_response_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) network.get = function(url, callback, errorCallback, headers)
local id = network.__get(url, headers) return request(url, {
if callback then method = "GET",
_http_response_callbacks[id] = callback headers = headers,
end on_response = function(response)
if errorCallback then if response.status / 100 == 2 then
_http_error_callbacks[id] = errorCallback return callback(response.body)
end else
return errorCallback(response.status, response.body)
end
end,
on_error = function(response)
errorCallback(response.status, response.body)
end,
follow_location = true,
})
end end
network.get_binary = function(url, callback, errorCallback, headers) network.get_binary = function(url, callback, errorCallback, headers)
local id = network.__get_binary(url, headers) return request(url, {
if callback then method = "GET",
_http_response_callbacks[id] = callback headers = headers,
end on_response = callback and (function (response)
if errorCallback then if response.code / 100 == 2 then
_http_error_callbacks[id] = errorCallback return callback(Bytearray(response.body))
end else
return errorCallback(response.status, response.body)
end
end),
follow_location = true,
})
end end
network.post = function(url, data, callback, errorCallback, headers) network.post = function(url, body, callback, errorCallback, headers)
local id = network.__post(url, data, headers) return request(url, {
if callback then method = "POST",
_http_response_callbacks[id] = callback headers = table.extend({
end "Content-Type: application/json"
if errorCallback then }, headers),
_http_error_callbacks[id] = errorCallback body = body,
end 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 end
network.tcp_open = function (port, handler) 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) _tcp_server_callbacks[socket.id] = function(id)
handler(setmetatable({id=id}, Socket)) handler(setmetatable({id=id}, Socket))
@ -141,7 +181,7 @@ end
network.tcp_connect = function(address, port, callback, errorCallback) network.tcp_connect = function(address, port, callback, errorCallback)
local socket = setmetatable({id=0}, Socket) 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 _tcp_client_callbacks[socket.id] = function() callback(socket) end
if errorCallback then if errorCallback then
_tcp_client_error_callbacks[socket.id] = function(message) errorCallback(socket, message) end _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" error "udp server cannot be opened without datagram handler"
end 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) _udp_server_callbacks[socket.id] = function(address, port, data)
datagramHandler(address, port, data, socket) datagramHandler(address, port, data, socket)
@ -169,7 +209,7 @@ network.udp_connect = function (address, port, datagramHandler, openCallback)
end end
local socket = setmetatable({id=0}, WriteableSocket) 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 _udp_client_datagram_callbacks[socket.id] = datagramHandler
if openCallback then if openCallback then
@ -239,20 +279,10 @@ network.__process_events = function()
end end
end end
elseif etype == RESPONSE then elseif etype == RESPONSE then
if event[2] / 100 == 2 then local callback = _http_response_callbacks[event[3]]
local callback = _http_response_callbacks[event[3]] _http_response_callbacks[event[3]] = nil
_http_response_callbacks[event[3]] = nil if callback then
_http_error_callbacks[event[3]] = nil callback(event[4])
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
end end
end end

View file

@ -3,6 +3,7 @@
#include "engine/Engine.hpp" #include "engine/Engine.hpp"
#include "network/Network.hpp" #include "network/Network.hpp"
#include "devtools/Project.hpp" #include "devtools/Project.hpp"
#include "util/stringutil.hpp"
#include <variant> #include <variant>
#include <utility> #include <utility>
@ -27,7 +28,7 @@ struct ResponseEventDto {
int status; int status;
bool binary; bool binary;
int requestId; int requestId;
std::vector<char> bytes; network::HttpResponse response;
}; };
enum NetworkDatagramSide { enum NetworkDatagramSide {
@ -85,74 +86,55 @@ static std::vector<std::string> read_headers(lua::State* L, int index) {
static int request_id = 1; static int request_id = 1;
static int perform_get(lua::State* L, network::Network& network, bool binary) { static int l_request(lua::State* L, network::Network& network) {
std::string url(lua::require_lstring(L, 1)); network::HttpRequest request {};
auto headers = read_headers(L, 2); request.url = lua::require_lstring(L, 1);
int currentRequestId = request_id++; if (!lua::istable(L, 2)) {
throw std::runtime_error("table expected as argument #2");
network.get( }
url, if (lua::getfield(L, "method", 2)) {
[currentRequestId, binary](std::vector<char> bytes) { request.method = lua::require_string(L, -1);
push_event(NetworkEvent( lua::pop(L);
RESPONSE, }
ResponseEventDto { if (lua::getfield(L, "headers", 2)) {
200, binary, currentRequestId, std::move(bytes)} request.headers = read_headers(L, -1);
)); lua::pop(L);
}, }
[currentRequestId, binary](int code, std::vector<char> bytes) { if (lua::getfield(L, "body", 2)) {
push_event(NetworkEvent( if (lua::type(L, -1) == LUA_TCDATA) {
RESPONSE, request.body = lua::bytearray_as_string(L, -1);
ResponseEventDto { } else {
code, binary, currentRequestId, std::move(bytes)} request.body = lua::require_lstring(L, -1);
)); }
}, lua::pop(L);
std::move(headers) }
); if (lua::getfield(L, "follow_location", 2)) {
return lua::pushinteger(L, currentRequestId); request.followLocation = lua::toboolean(L, -1);
} lua::pop(L);
}
static int l_get(lua::State* L, network::Network& network) { if (lua::getfield(L, "timeout_ms", 2)) {
return perform_get(L, network, false); request.timeoutMs = lua::tointeger(L, -1);
} lua::pop(L);
}
static int l_get_binary(lua::State* L, network::Network& network) { if (lua::getfield(L, "verify_ssl", 2)) {
return perform_get(L, network, true); request.verifySSL = lua::toboolean(L, -1);
} lua::pop(L);
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);
} }
auto headers = read_headers(L, 3);
int currentRequestId = request_id++; int currentRequestId = request_id++;
request.onResponse = [currentRequestId](network::HttpResponse response) {
push_event(NetworkEvent(
RESPONSE,
ResponseEventDto {
response.status,
false,
currentRequestId,
std::move(response)}
));
};
network.post( network.request(std::move(request));
url,
string,
[currentRequestId](std::vector<char> bytes) {
push_event(NetworkEvent(
RESPONSE,
ResponseEventDto {
200, false, currentRequestId, std::move(bytes)}
));
},
[currentRequestId](int code, std::vector<char> bytes) {
push_event(NetworkEvent(
RESPONSE,
ResponseEventDto {
code, false, currentRequestId, std::move(bytes)}
));
},
std::move(headers)
);
return lua::pushinteger(L, currentRequestId); return lua::pushinteger(L, currentRequestId);
} }
@ -519,11 +501,31 @@ static int l_pull_events(lua::State* L) {
lua::pushinteger(L, dto.requestId); lua::pushinteger(L, dto.requestId);
lua::rawseti(L, 3); lua::rawseti(L, 3);
lua::createtable(L, 0, 2);
lua::pushinteger(L, dto.response.status);
lua::setfield(L, "status");
if (dto.binary) { 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 { } 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); lua::rawseti(L, 4);
break; break;
} }
@ -562,9 +564,7 @@ int wrap(lua_State* L) {
} }
const luaL_Reg networklib[] = { const luaL_Reg networklib[] = {
{"__get", wrap<l_get>}, {"__request", wrap<l_request>},
{"__get_binary", wrap<l_get_binary>},
{"__post", wrap<l_post>},
{"get_total_upload", wrap<l_get_total_upload>}, {"get_total_upload", wrap<l_get_total_upload>},
{"get_total_download", wrap<l_get_total_download>}, {"get_total_download", wrap<l_get_total_download>},
{"find_free_port", wrap<l_find_free_port>}, {"find_free_port", wrap<l_find_free_port>},

View file

@ -4,7 +4,6 @@
#define NOMINMAX #define NOMINMAX
#include <curl/curl.h> #include <curl/curl.h>
#include <queue>
using namespace network; using namespace network;
@ -23,97 +22,96 @@ static size_t write_callback(
return size * nmemb; return size * nmemb;
} }
enum class RequestType { static size_t header_callback(
GET, POST char* buffer, size_t size, size_t nitems, void* userdata
}; ) {
auto* headers = static_cast<std::vector<std::string>*>(userdata);
size_t len = size * nitems;
std::string header(buffer, len);
struct Request { while (!header.empty() &&
RequestType type; (header.back() == '\r' || header.back() == '\n')) {
std::string url; header.pop_back();
OnResponse onResponse; }
OnReject onReject;
long maxSize; headers->push_back(std::move(header));
bool followLocation = false; return len;
std::string data; }
struct ProcessingRequest {
CURLM* multiHandle;
CURL* curl;
HttpRequest request;
std::vector<char> buffer;
std::vector<std::string> headers; std::vector<std::string> 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 { class CurlRequests : public Requests {
CURLM* multiHandle; CURLM* multiHandle;
CURL* curl; std::vector<std::unique_ptr<ProcessingRequest>> requests;
size_t totalUpload = 0; size_t totalUpload = 0;
size_t totalDownload = 0; size_t totalDownload = 0;
OnResponse onResponse;
OnReject onReject;
std::vector<char> buffer;
std::string url;
std::queue<Request> requests;
public: public:
CurlRequests(CURLM* multiHandle, CURL* curl) CurlRequests(CURLM* multiHandle) : multiHandle(multiHandle) {
: multiHandle(multiHandle), curl(curl) {
} }
virtual ~CurlRequests() { virtual ~CurlRequests() {
curl_multi_remove_handle(multiHandle, curl); requests.clear();
curl_easy_cleanup(curl);
curl_multi_cleanup(multiHandle); curl_multi_cleanup(multiHandle);
} }
void get(
const std::string& url, void request(HttpRequest request) override {
OnResponse onResponse,
OnReject onReject,
std::vector<std::string> headers,
long maxSize
) override {
Request request {
RequestType::GET,
url,
onResponse,
onReject,
maxSize,
true,
"",
std::move(headers)};
processRequest(std::move(request)); processRequest(std::move(request));
} }
void post( void processRequest(HttpRequest request) {
const std::string& url, auto entry = std::make_unique<ProcessingRequest>(multiHandle);
const std::string& data, auto curl = entry->curl;
OnResponse onResponse,
OnReject onReject=nullptr,
std::vector<std::string> 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(Request request) { curl_easy_setopt(curl, CURLOPT_URL, request.url.c_str());
if (!url.empty()) { curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, request.method.c_str());
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_slist* hs = nullptr; curl_slist* hs = nullptr;
@ -121,22 +119,25 @@ public:
hs = curl_slist_append(hs, header.c_str()); hs = curl_slist_append(hs, header.c_str());
} }
switch (request.type) { curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(request.body.length()));
case RequestType::GET: if (!request.body.empty()) {
break; curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, request.body.data());
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_SSL_VERIFYPEER, static_cast<long>(request.verifySSL));
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, static_cast<long>(request.verifySSL));
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hs); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hs);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, request.followLocation); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, static_cast<long>(request.followLocation));
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); 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"); 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) { if (request.maxSize == 0) {
curl_easy_setopt( curl_easy_setopt(
curl, CURLOPT_MAXFILESIZE, std::numeric_limits<long>::max() curl, CURLOPT_MAXFILESIZE, std::numeric_limits<long>::max()
@ -144,69 +145,105 @@ public:
} else { } else {
curl_easy_setopt(curl, CURLOPT_MAXFILESIZE, request.maxSize); curl_easy_setopt(curl, CURLOPT_MAXFILESIZE, request.maxSize);
} }
curl_multi_add_handle(multiHandle, curl); {
int running; CURLMcode res = curl_multi_add_handle(multiHandle, curl);
CURLMcode res = curl_multi_perform(multiHandle, &running); if (res != CURLM_OK) {
if (res != CURLM_OK) { auto message = curl_multi_strerror(res);
auto message = curl_multi_strerror(res); logger.error() << message << " (" << request.url << ")";
logger.error() << message << " (" << url << ")"; if (request.onResponse) {
if (onReject) { request.onResponse({0, {}, {}});
onReject(HTTP_BAD_GATEWAY, {}); }
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 { void update() override {
int messagesLeft; if (CURLMcode res = curl_multi_poll(
int running; multiHandle,
CURLMsg* msg; nullptr,
CURLMcode res = curl_multi_perform(multiHandle, &running); 0,
if (res != CURLM_OK) { 0,
nullptr
)) {
auto message = curl_multi_strerror(res); auto message = curl_multi_strerror(res);
logger.error() << message << " (" << url << ")"; logger.error() << message;
if (onReject) {
onReject(HTTP_BAD_GATEWAY, {});
}
curl_multi_remove_handle(multiHandle, curl);
url = "";
return; return;
} }
if ((msg = curl_multi_info_read(multiHandle, &messagesLeft)) != nullptr) {
if(msg->msg == CURLMSG_DONE) { int messagesLeft;
curl_multi_remove_handle(multiHandle, curl); int running;
} if (CURLMcode res = curl_multi_perform(multiHandle, &running)) {
int response; auto message = curl_multi_strerror(res);
curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &response); logger.error() << message;
if (response == HTTP_OK) { return;
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 = "";
} }
if (url.empty() && !requests.empty()) { CURLMsg* msg = curl_multi_info_read(multiHandle, &messagesLeft);
auto request = std::move(requests.front()); if (msg == nullptr || msg->msg != CURLMSG_DONE) {
requests.pop(); return;
processRequest(std::move(request)); }
auto curl = msg->easy_handle;
auto found = std::find_if(
requests.begin(),
requests.end(),
[curl](const std::unique_ptr<ProcessingRequest>& 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<int>(response),
std::move(headers),
std::vector<char>(
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<int>(response),
std::move(headers),
std::move(entry->buffer),
});
}
} }
} }
@ -219,16 +256,11 @@ public:
} }
static std::unique_ptr<CurlRequests> create() { static std::unique_ptr<CurlRequests> create() {
auto curl = curl_easy_init();
if (curl == nullptr) {
throw std::runtime_error("could not initialzie cURL");
}
auto multiHandle = curl_multi_init(); auto multiHandle = curl_multi_init();
if (multiHandle == nullptr) { if (multiHandle == nullptr) {
curl_easy_cleanup(curl); throw std::runtime_error("could not initialize cURL-multi");
throw std::runtime_error("could not initialzie cURL-multi");
} }
return std::make_unique<CurlRequests>(multiHandle, curl); return std::make_unique<CurlRequests>(multiHandle);
} }
}; };

View file

@ -51,27 +51,8 @@ Network::Network(std::unique_ptr<Requests> requests)
Network::~Network() = default; Network::~Network() = default;
void Network::get( void Network::request(HttpRequest request) {
const std::string& url, requests->request(std::move(request));
OnResponse onResponse,
OnReject onReject,
std::vector<std::string> 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<std::string> headers,
long maxSize
) {
requests->post(
url, fieldsData, onResponse, onReject, std::move(headers), maxSize
);
} }
Connection* Network::getConnection(u64id_t id, bool includePrivate) { Connection* Network::getConnection(u64id_t id, bool includePrivate) {

View file

@ -68,22 +68,7 @@ namespace network {
Network(std::unique_ptr<Requests> requests); Network(std::unique_ptr<Requests> requests);
~Network(); ~Network();
void get( void request(HttpRequest request);
const std::string& url,
OnResponse onResponse,
OnReject onReject = nullptr,
std::vector<std::string> headers = {},
long maxSize=0
);
void post(
const std::string& url,
const std::string& fieldsData,
OnResponse onResponse,
OnReject onReject = nullptr,
std::vector<std::string> headers = {},
long maxSize=0
);
[[nodiscard]] Connection* getConnection(u64id_t id, bool includePrivate); [[nodiscard]] Connection* getConnection(u64id_t id, bool includePrivate);
[[nodiscard]] Server* getServer(u64id_t id, bool includePrivate) const; [[nodiscard]] Server* getServer(u64id_t id, bool includePrivate) const;

View file

@ -10,33 +10,38 @@
#include <mutex> #include <mutex>
namespace network { namespace network {
using OnResponse = std::function<void(std::vector<char>)>; struct HttpResponse;
using OnReject = std::function<void(int, std::vector<char>)>;
using OnResponse = std::function<void(HttpResponse)>;
using ConnectCallback = std::function<void(u64id_t, u64id_t)>; using ConnectCallback = std::function<void(u64id_t, u64id_t)>;
using ConnectErrorCallback = std::function<void(u64id_t, std::string)>; using ConnectErrorCallback = std::function<void(u64id_t, std::string)>;
using ServerDatagramCallback = std::function<void(u64id_t sid, const std::string& addr, int port, const char* buffer, size_t length)>; using ServerDatagramCallback = std::function<void(u64id_t sid, const std::string& addr, int port, const char* buffer, size_t length)>;
using ClientDatagramCallback = std::function<void(u64id_t cid, const char* buffer, size_t length)>; using ClientDatagramCallback = std::function<void(u64id_t cid, const char* buffer, size_t length)>;
struct HttpRequest {
std::string method;
std::string url;
std::string body;
std::vector<std::string> headers;
OnResponse onResponse;
bool followLocation = false;
bool verifySSL = true;
long maxSize = -1;
long timeoutMs = 0;
};
struct HttpResponse {
int status;
std::vector<std::string> headers;
std::vector<char> body;
};
class Requests { class Requests {
public: public:
virtual ~Requests() {} virtual ~Requests() {}
virtual void get( virtual void request(HttpRequest request) = 0;
const std::string& url,
OnResponse onResponse,
OnReject onReject=nullptr,
std::vector<std::string> headers = {},
long maxSize=0
) = 0;
virtual void post(
const std::string& url,
const std::string& data,
OnResponse onResponse,
OnReject onReject=nullptr,
std::vector<std::string> headers = {},
long maxSize=0
) = 0;
[[nodiscard]] virtual size_t getTotalUpload() const = 0; [[nodiscard]] virtual size_t getTotalUpload() const = 0;
[[nodiscard]] virtual size_t getTotalDownload() const = 0; [[nodiscard]] virtual size_t getTotalDownload() const = 0;

View file

@ -543,6 +543,14 @@ static std::basic_string<CharT> upper_case(const std::basic_string<CharT>& str)
return result; 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) { std::wstring util::lower_case(const std::wstring& str) {
return ::lower_case(str); return ::lower_case(str);
} }

View file

@ -101,6 +101,8 @@ namespace util {
std::string capitalized(const std::string& str); 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 lower_case(const std::wstring& str);
std::wstring upper_case(const std::wstring& str); std::wstring upper_case(const std::wstring& str);
std::wstring capitalized(const std::wstring& str); std::wstring capitalized(const std::wstring& str);

View file

@ -6,18 +6,21 @@
TEST(curltest, curltest) { TEST(curltest, curltest) {
NetworkSettings settings {}; NetworkSettings settings {};
auto network = network::Network::create(settings); auto network = network::Network::create(settings);
network->get(
"https://raw.githubusercontent.com/MihailRis/VoxelEngine-Cpp/refs/" network::HttpRequest request {};
"heads/curl/res/content/base/blocks/lamp.json", request.url = "https://raw.githubusercontent.com/MihailRis/VoxelEngine-Cpp/refs/"
[](std::vector<char> data) { "heads/curl/res/content/base/blocks/lamp.json";
if (data.empty()) { request.onResponse = [](network::HttpResponse response) {
return; if (response.body.empty()) {
} return;
auto view = std::string_view(data.data(), data.size()); }
auto value = json::parse(view); auto view =
std::cout << value << std::endl; std::string_view(response.body.data(), response.body.size());
}, [](auto, auto){} 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 << "upload: " << network->getTotalUpload() << " B" << std::endl;
std::cout << "download: " << network->getTotalDownload() << " B" << std::endl; std::cout << "download: " << network->getTotalDownload() << " B" << std::endl;
} }

View file

@ -16,7 +16,8 @@ struct Config {
fs::path directory; fs::path directory;
fs::path resDir {"res"}; fs::path resDir {"res"};
fs::path workingDir {"."}; fs::path workingDir {"."};
std::string memchecker = "valgrind"; std::string memchecker = "";
std::string debugger = "";
bool outputAlways = false; bool outputAlways = false;
}; };
@ -30,6 +31,7 @@ static bool perform_keyword(
std::cout << " --tests <path>, -d <path> = tests directory path\n"; std::cout << " --tests <path>, -d <path> = tests directory path\n";
std::cout << " --res <path>, -r <path> = 'res' directory path\n"; std::cout << " --res <path>, -r <path> = 'res' directory path\n";
std::cout << " --user <path>, -u <path> = user directory path\n"; std::cout << " --user <path>, -u <path> = user directory path\n";
std::cout << " --debugger <command> = debugger with run command\n";
std::cout << " --memchecker <path> = path to valgrind\n"; std::cout << " --memchecker <path> = path to valgrind\n";
std::cout << " --output-always = always show tests output\n"; std::cout << " --output-always = always show tests output\n";
std::cout << std::endl; std::cout << std::endl;
@ -44,6 +46,8 @@ static bool perform_keyword(
config.workingDir = fs::path(reader.next()); config.workingDir = fs::path(reader.next());
} else if (keyword == "--output-always") { } else if (keyword == "--output-always") {
config.outputAlways = true; config.outputAlways = true;
} else if (keyword == "--debugger") {
config.debugger = reader.next();
} else if (keyword == "--memchecker") { } else if (keyword == "--memchecker") {
config.memchecker = reader.next(); config.memchecker = reader.next();
} else { } else {
@ -172,7 +176,7 @@ static std::string fix_path(std::string s) {
return 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::duration_cast;
using std::chrono::high_resolution_clock; using std::chrono::high_resolution_clock;
using std::chrono::milliseconds; 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(); auto name = path.stem();
std::stringstream ss; std::stringstream ss;
if (memcheck) { if (debugRun) {
ss << config.memchecker << " --log-file=" if (!config.memchecker.empty()) {
<< fix_path(memcheckLogFile.string()) << " "; 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 << fs::canonical(config.executable) << " --headless";
ss << " --test " << fix_path(path.string()); ss << " --test " << fix_path(path.string());
@ -203,7 +214,7 @@ static bool run_test(const Config& config, const fs::path& path, bool memcheck =
.count(); .count();
if (code) { if (code) {
if (memcheck) { if (debugRun) {
// valgrind-specific output // valgrind-specific output
display_segfault_valgrind(memcheckLogFile, name, std::cerr); display_segfault_valgrind(memcheckLogFile, name, std::cerr);
fs::remove(memcheckLogFile); fs::remove(memcheckLogFile);