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
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:

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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<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
-- Performs a GET request to the specified URL.
network.get(

View file

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

View file

@ -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

View file

@ -3,6 +3,7 @@
#include "engine/Engine.hpp"
#include "network/Network.hpp"
#include "devtools/Project.hpp"
#include "util/stringutil.hpp"
#include <variant>
#include <utility>
@ -27,7 +28,7 @@ struct ResponseEventDto {
int status;
bool binary;
int requestId;
std::vector<char> bytes;
network::HttpResponse response;
};
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 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<char> bytes) {
push_event(NetworkEvent(
RESPONSE,
ResponseEventDto {
200, binary, currentRequestId, std::move(bytes)}
));
},
[currentRequestId, binary](int code, std::vector<char> 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<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)
);
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<l_get>},
{"__get_binary", wrap<l_get_binary>},
{"__post", wrap<l_post>},
{"__request", wrap<l_request>},
{"get_total_upload", wrap<l_get_total_upload>},
{"get_total_download", wrap<l_get_total_download>},
{"find_free_port", wrap<l_find_free_port>},

View file

@ -4,7 +4,6 @@
#define NOMINMAX
#include <curl/curl.h>
#include <queue>
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<std::vector<std::string>*>(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<char> buffer;
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 {
CURLM* multiHandle;
CURL* curl;
std::vector<std::unique_ptr<ProcessingRequest>> requests;
size_t totalUpload = 0;
size_t totalDownload = 0;
OnResponse onResponse;
OnReject onReject;
std::vector<char> buffer;
std::string url;
std::queue<Request> 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<std::string> 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<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(HttpRequest request) {
auto entry = std::make_unique<ProcessingRequest>(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<long>(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<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_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_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<long>::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<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() {
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<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;
void Network::get(
const std::string& url,
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
);
void Network::request(HttpRequest request) {
requests->request(std::move(request));
}
Connection* Network::getConnection(u64id_t id, bool includePrivate) {

View file

@ -68,22 +68,7 @@ namespace network {
Network(std::unique_ptr<Requests> requests);
~Network();
void get(
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
);
void request(HttpRequest request);
[[nodiscard]] Connection* getConnection(u64id_t id, bool includePrivate);
[[nodiscard]] Server* getServer(u64id_t id, bool includePrivate) const;

View file

@ -10,33 +10,38 @@
#include <mutex>
namespace network {
using OnResponse = std::function<void(std::vector<char>)>;
using OnReject = std::function<void(int, std::vector<char>)>;
struct HttpResponse;
using OnResponse = std::function<void(HttpResponse)>;
using ConnectCallback = std::function<void(u64id_t, u64id_t)>;
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 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 {
public:
virtual ~Requests() {}
virtual void get(
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;
virtual void request(HttpRequest request) = 0;
[[nodiscard]] virtual size_t getTotalUpload() 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;
}
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);
}

View file

@ -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);

View file

@ -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<char> 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;
}

View file

@ -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 <path>, -d <path> = tests 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 << " --debugger <command> = debugger with run command\n";
std::cout << " --memchecker <path> = 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);