mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-09-06 09:55:11 -04:00
Mods: NetService and WebSocketService (#2385)
This commit is contained in:
+91
-217
@@ -1,107 +1,80 @@
|
||||
#if _WIN32
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
using socket_t = SOCKET;
|
||||
static void closeSocket(socket_t s) {
|
||||
LINGER li{1, 0};
|
||||
setsockopt(s, SOL_SOCKET, SO_LINGER, reinterpret_cast<const char*>(&li), sizeof(li));
|
||||
closesocket(s);
|
||||
}
|
||||
static int socketError(socket_t s) {
|
||||
int err = 0;
|
||||
int len = sizeof(err);
|
||||
getsockopt(s, SOL_SOCKET, SO_ERROR, reinterpret_cast<char*>(&err), &len);
|
||||
return err;
|
||||
}
|
||||
static constexpr int kSendFlags = 0;
|
||||
#else
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
using socket_t = int;
|
||||
static void closeSocket(socket_t s) {
|
||||
struct linger li{1, 0};
|
||||
setsockopt(s, SOL_SOCKET, SO_LINGER, &li, sizeof(li));
|
||||
close(s);
|
||||
}
|
||||
static int socketError(socket_t s) {
|
||||
int err = 0;
|
||||
socklen_t len = sizeof(err);
|
||||
getsockopt(s, SOL_SOCKET, SO_ERROR, &err, &len);
|
||||
return err;
|
||||
}
|
||||
#ifndef INVALID_SOCKET
|
||||
#define INVALID_SOCKET -1
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
static constexpr int kSendFlags = 0;
|
||||
#else
|
||||
static constexpr int kSendFlags = MSG_NOSIGNAL;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include <cstdio>
|
||||
#include "dusk/livesplit.h"
|
||||
|
||||
#include "borealis/net.hpp"
|
||||
|
||||
#include "f_op/f_op_overlap_mng.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string>
|
||||
|
||||
namespace dusk::speedrun {
|
||||
namespace {
|
||||
|
||||
static bool running = false;
|
||||
static bool startPending = false;
|
||||
static uint64_t frameCount = 0;
|
||||
static socket_t sock = INVALID_SOCKET;
|
||||
static bool wasLoading = false;
|
||||
static bool connected = false;
|
||||
static bool connectPending = false;
|
||||
static bool disconnectPending = false;
|
||||
static uint32_t idleProbeCounter = 0;
|
||||
static uint32_t reconnectCounter = 0;
|
||||
static char storedHost[64] = "127.0.0.1";
|
||||
static int storedPort = 16834;
|
||||
bool running = false;
|
||||
bool startPending = false;
|
||||
uint64_t frameCount = 0;
|
||||
bool wasLoading = false;
|
||||
bool connected = false;
|
||||
bool connectPending = false;
|
||||
bool disconnectPending = false;
|
||||
uint32_t reconnectCounter = 0;
|
||||
std::string storedEndpoint = "tcp://127.0.0.1:16834";
|
||||
std::unique_ptr<borealis::net::Context> netContext;
|
||||
borealis::net::SocketId socketId = 0;
|
||||
|
||||
static void sendCmd(const char* cmd) {
|
||||
if (sock == INVALID_SOCKET) {
|
||||
void send_cmd(const char* command) {
|
||||
if (!netContext || !connected || socketId == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
char msg[64];
|
||||
const int len = snprintf(msg, sizeof(msg), "%s\r\n", cmd);
|
||||
if (len <= 0 || len >= static_cast<int>(sizeof(msg))) {
|
||||
char message[64];
|
||||
const int length = snprintf(message, sizeof(message), "%s\r\n", command);
|
||||
if (length <= 0 || length >= static_cast<int>(sizeof(message))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (send(sock, msg, len, kSendFlags) >= 0) {
|
||||
if (!connected) {
|
||||
connected = connectPending = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
const int err = WSAGetLastError();
|
||||
if (err == WSAEWOULDBLOCK || err == WSAENOTCONN) {
|
||||
return;
|
||||
}
|
||||
#else
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == ENOTCONN) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (connected) {
|
||||
disconnectPending = true;
|
||||
}
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
connected = connectPending = false;
|
||||
reconnectCounter = 0;
|
||||
const auto chars = std::span<const char>{message, static_cast<size_t>(length)};
|
||||
netContext->send(socketId, std::as_bytes(chars));
|
||||
}
|
||||
|
||||
void reconnect() {
|
||||
netContext.reset();
|
||||
netContext = std::make_unique<borealis::net::Context>();
|
||||
connected = false;
|
||||
connectPending = false;
|
||||
socketId = netContext->connect(storedEndpoint);
|
||||
}
|
||||
|
||||
void poll_network() {
|
||||
if (!netContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
borealis::net::Event event;
|
||||
while (netContext->poll(event)) {
|
||||
if (event.id != socketId) {
|
||||
continue;
|
||||
}
|
||||
if (event.kind == borealis::net::Event::Kind::Connected) {
|
||||
connected = true;
|
||||
connectPending = true;
|
||||
send_cmd("initgametime");
|
||||
} else if (event.kind == borealis::net::Event::Kind::Closed) {
|
||||
if (connected) {
|
||||
disconnectPending = true;
|
||||
}
|
||||
connected = false;
|
||||
connectPending = false;
|
||||
socketId = 0;
|
||||
reconnectCounter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint64_t getFrameCount() {
|
||||
return frameCount;
|
||||
}
|
||||
@@ -111,10 +84,9 @@ void onGameFrame() {
|
||||
return;
|
||||
}
|
||||
|
||||
bool loading = fopOvlpM_IsDoingReq() != 0;
|
||||
|
||||
const bool loading = fopOvlpM_IsDoingReq() != 0;
|
||||
if (loading != wasLoading) {
|
||||
sendCmd(loading ? "pausegametime" : "unpausegametime");
|
||||
send_cmd(loading ? "pausegametime" : "unpausegametime");
|
||||
wasLoading = loading;
|
||||
}
|
||||
|
||||
@@ -141,172 +113,74 @@ void reset() {
|
||||
startPending = false;
|
||||
frameCount = 0;
|
||||
wasLoading = false;
|
||||
sendCmd("reset");
|
||||
}
|
||||
|
||||
static void reconnect() {
|
||||
if (sock != INVALID_SOCKET) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
}
|
||||
connected = connectPending = false;
|
||||
|
||||
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (sock == INVALID_SOCKET) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
u_long nb = 1;
|
||||
if (ioctlsocket(sock, FIONBIO, &nb) != 0) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
return;
|
||||
}
|
||||
#else
|
||||
const int fl = fcntl(sock, F_GETFL, 0);
|
||||
if (fl < 0 || fcntl(sock, F_SETFL, fl | O_NONBLOCK) < 0) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
{
|
||||
int opt = 1;
|
||||
setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt));
|
||||
}
|
||||
#endif
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(static_cast<uint16_t>(storedPort));
|
||||
if (inet_pton(AF_INET, storedHost, &addr.sin_addr) != 1) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
return;
|
||||
}
|
||||
|
||||
const int cr = connect(sock, reinterpret_cast<sockaddr*>(&addr), sizeof(addr));
|
||||
#if _WIN32
|
||||
const bool connectPending_ = cr < 0 && WSAGetLastError() == WSAEWOULDBLOCK;
|
||||
#else
|
||||
const bool connectPending_ = cr < 0 && errno == EINPROGRESS;
|
||||
#endif
|
||||
if (cr != 0 && !connectPending_) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
}
|
||||
send_cmd("reset");
|
||||
}
|
||||
|
||||
void connectLiveSplit(const char* host, int port) {
|
||||
#if _WIN32
|
||||
WSADATA wd{};
|
||||
WSAStartup(MAKEWORD(2, 2), &wd);
|
||||
#endif
|
||||
snprintf(storedHost, sizeof(storedHost), "%s", host);
|
||||
storedPort = port;
|
||||
std::string endpointHost = host;
|
||||
if (endpointHost.find(':') != std::string::npos &&
|
||||
!(endpointHost.starts_with('[') && endpointHost.ends_with(']')))
|
||||
{
|
||||
endpointHost = '[' + endpointHost + ']';
|
||||
}
|
||||
storedEndpoint = "tcp://" + endpointHost + ':' + std::to_string(port);
|
||||
reconnect();
|
||||
}
|
||||
|
||||
void disconnectLiveSplit() {
|
||||
if (sock != INVALID_SOCKET) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
}
|
||||
connected = connectPending = disconnectPending = false;
|
||||
netContext.reset();
|
||||
socketId = 0;
|
||||
connected = false;
|
||||
connectPending = false;
|
||||
disconnectPending = false;
|
||||
}
|
||||
|
||||
bool consumeConnectedEvent() {
|
||||
bool v = connectPending;
|
||||
const bool value = connectPending;
|
||||
connectPending = false;
|
||||
return v;
|
||||
return value;
|
||||
}
|
||||
|
||||
bool consumeDisconnectedEvent() {
|
||||
bool v = disconnectPending;
|
||||
const bool value = disconnectPending;
|
||||
disconnectPending = false;
|
||||
return v;
|
||||
return value;
|
||||
}
|
||||
|
||||
void updateLiveSplit() {
|
||||
if (sock == INVALID_SOCKET) {
|
||||
poll_network();
|
||||
if (socketId == 0) {
|
||||
if ((reconnectCounter++ % 30) == 0) {
|
||||
reconnect();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
fd_set writefds, errorfds;
|
||||
FD_ZERO(&writefds);
|
||||
FD_ZERO(&errorfds);
|
||||
FD_SET(sock, &writefds);
|
||||
FD_SET(sock, &errorfds);
|
||||
timeval tv{0, 0};
|
||||
#if _WIN32
|
||||
const int r = select(0, nullptr, &writefds, &errorfds, &tv);
|
||||
#else
|
||||
const int r = select(sock + 1, nullptr, &writefds, &errorfds, &tv);
|
||||
#endif
|
||||
if (r < 0 || FD_ISSET(sock, &errorfds) || socketError(sock) != 0) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
reconnectCounter = 0;
|
||||
return;
|
||||
}
|
||||
if (!FD_ISSET(sock, &writefds)) {
|
||||
return;
|
||||
}
|
||||
sendCmd("initgametime");
|
||||
return;
|
||||
}
|
||||
|
||||
if (startPending) {
|
||||
startPending = false;
|
||||
sendCmd("initgametime");
|
||||
sendCmd("reset");
|
||||
sendCmd("starttimer");
|
||||
send_cmd("initgametime");
|
||||
send_cmd("reset");
|
||||
send_cmd("starttimer");
|
||||
}
|
||||
|
||||
if (!running) {
|
||||
if ((idleProbeCounter++ % 60) == 0) {
|
||||
char buf;
|
||||
const int r = recv(sock, &buf, 1, 0);
|
||||
if (r == 0
|
||||
#if _WIN32
|
||||
|| (r < 0 && WSAGetLastError() != WSAEWOULDBLOCK)
|
||||
#else
|
||||
|| (r < 0 && errno != EAGAIN && errno != EWOULDBLOCK)
|
||||
#endif
|
||||
)
|
||||
{
|
||||
if (connected) {
|
||||
disconnectPending = true;
|
||||
}
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
connected = connectPending = false;
|
||||
reconnectCounter = 0;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const uint64_t totalMs = frameCount * 1000 / 30;
|
||||
const uint64_t totalSec = totalMs / 1000;
|
||||
char cmd[32];
|
||||
snprintf(cmd, sizeof(cmd), "setgametime %u:%02u:%02u.%03u",
|
||||
char command[32];
|
||||
snprintf(command, sizeof(command), "setgametime %u:%02u:%02u.%03u",
|
||||
static_cast<uint32_t>(totalSec / 3600), static_cast<uint32_t>((totalSec / 60) % 60),
|
||||
static_cast<uint32_t>(totalSec % 60), static_cast<uint32_t>(totalMs % 1000));
|
||||
sendCmd(cmd);
|
||||
send_cmd(command);
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
disconnectLiveSplit();
|
||||
#if _WIN32
|
||||
WSACleanup();
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace dusk::speedrun
|
||||
|
||||
@@ -1027,7 +1027,12 @@ void ModLoader::deactivate_mod(LoadedMod& mod) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "{} failed: {}", shutdownName,
|
||||
lifecycle_error_message(shutdownName, result, error));
|
||||
}
|
||||
} catch (const std::exception& exception) {
|
||||
log::write(
|
||||
mod.metadata.id, LOG_LEVEL_ERROR, "{} threw: {}", shutdownName, exception.what());
|
||||
} catch (...) {
|
||||
log::write(
|
||||
mod.metadata.id, LOG_LEVEL_ERROR, "{} threw an unknown exception", shutdownName);
|
||||
}
|
||||
}
|
||||
mod.initialized = false;
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
#include "dusk/mods/svc/actor.hpp"
|
||||
|
||||
#include "config.hpp"
|
||||
#include "internal.hpp"
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "dusk/mod_loader.hpp"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "internal.hpp"
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "dusk/camera_operators.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "config.hpp"
|
||||
|
||||
#include "internal.hpp"
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/config.hpp"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "slot_map.hpp"
|
||||
#include "internal.hpp"
|
||||
|
||||
#include <aurora/lib/window.hpp>
|
||||
#include <borealis/file_select.hpp>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
#include "dusk/game_mode.hpp"
|
||||
|
||||
#include "config.hpp"
|
||||
#include "internal.hpp"
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "dusk/mod_loader.hpp"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "internal.hpp"
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
#include <borealis/log.hpp>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "internal.hpp"
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
|
||||
+23
-83
@@ -1,21 +1,20 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "slot_map.hpp"
|
||||
#include "internal.hpp"
|
||||
#include "net.hpp"
|
||||
|
||||
#include "dusk/app_info.hpp"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/http.h"
|
||||
|
||||
#include <borealis/http.hpp>
|
||||
#include <borealis/io.hpp>
|
||||
#include <borealis/version.h>
|
||||
#include <borealis/url.hpp>
|
||||
#include <fmt/format.h>
|
||||
#include <xxhash.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
@@ -38,6 +37,15 @@ constexpr size_t MaxRequestBodyBytes = 16 * 1024 * 1024;
|
||||
constexpr size_t DefaultResponseBodyBytes = 1024 * 1024;
|
||||
constexpr size_t MaxResponseBodyBytes = 64 * 1024 * 1024;
|
||||
constexpr std::chrono::milliseconds DefaultTimeout{10000};
|
||||
constexpr std::string_view ReservedHeaders[]{
|
||||
"User-Agent",
|
||||
"Host",
|
||||
"Content-Length",
|
||||
"Connection",
|
||||
"Accept-Encoding",
|
||||
"Range",
|
||||
"If-Range",
|
||||
};
|
||||
|
||||
struct PendingRequest {
|
||||
HttpCompleteFn callback = nullptr;
|
||||
@@ -52,53 +60,9 @@ static_assert(std::is_nothrow_move_constructible_v<PendingRequest>);
|
||||
|
||||
SlotMap<PendingRequest> s_requests;
|
||||
|
||||
bool ascii_iequals(std::string_view left, std::string_view right) {
|
||||
return left.size() == right.size() && std::ranges::equal(left, right, [](char a, char b) {
|
||||
return std::tolower(static_cast<unsigned char>(a)) ==
|
||||
std::tolower(static_cast<unsigned char>(b));
|
||||
});
|
||||
}
|
||||
|
||||
bool is_reserved_header(std::string_view name) {
|
||||
constexpr std::string_view reserved[]{
|
||||
"User-Agent",
|
||||
"Host",
|
||||
"Content-Length",
|
||||
"Connection",
|
||||
"Accept-Encoding",
|
||||
"Range",
|
||||
"If-Range",
|
||||
};
|
||||
return std::ranges::any_of(
|
||||
reserved, [&](std::string_view value) { return ascii_iequals(name, value); });
|
||||
}
|
||||
|
||||
bool valid_header_name(std::string_view name) {
|
||||
constexpr std::string_view separators{"()<>@,;:\\\"/[]?={} \t"};
|
||||
return !name.empty() && std::ranges::all_of(name, [&](unsigned char value) {
|
||||
return value > 32 && value < 127 &&
|
||||
separators.find(static_cast<char>(value)) == std::string_view::npos;
|
||||
});
|
||||
}
|
||||
|
||||
bool valid_url(std::string_view url) {
|
||||
constexpr std::string_view scheme{"https://"};
|
||||
if (!url.starts_with(scheme) || url.size() <= scheme.size() || url.size() > MaxUrlBytes) {
|
||||
return false;
|
||||
}
|
||||
if (std::ranges::any_of(url, [](unsigned char value) { return value <= 32 || value == 127; })) {
|
||||
return false;
|
||||
}
|
||||
const auto authorityEnd = url.find_first_of("/?#", scheme.size());
|
||||
const auto authority = url.substr(scheme.size(), authorityEnd - scheme.size());
|
||||
return !authority.empty();
|
||||
}
|
||||
|
||||
bool declares_http_import(const LoadedMod& mod) {
|
||||
return std::ranges::any_of(
|
||||
mod.manifestInfo.imports, [](const ModManifestInfo::Import& serviceImport) {
|
||||
return serviceImport.id == HTTP_SERVICE_ID;
|
||||
});
|
||||
const auto parsed = url.size() <= MaxUrlBytes ? borealis::url::parse(url) : std::nullopt;
|
||||
return parsed && parsed->scheme == "https";
|
||||
}
|
||||
|
||||
std::filesystem::path normalized_absolute(const std::filesystem::path& path, std::error_code& ec) {
|
||||
@@ -311,14 +275,8 @@ void http_frame_begin() {
|
||||
.download_path = downloadSucceeded ? publishedPath.c_str() : nullptr,
|
||||
};
|
||||
|
||||
try {
|
||||
callback(owner->context.get(), handle, &snapshot, userData);
|
||||
} catch (const std::exception& exception) {
|
||||
fail_mod(*owner, MOD_ERROR,
|
||||
std::string{"exception in HTTP completion callback: "} + exception.what());
|
||||
} catch (...) {
|
||||
fail_mod(*owner, MOD_ERROR, "unknown exception in HTTP completion callback");
|
||||
}
|
||||
guarded_callback(*owner, "HTTP completion callback",
|
||||
[&] { callback(owner->context.get(), handle, &snapshot, userData); });
|
||||
s_requests.erase(handle);
|
||||
}
|
||||
}
|
||||
@@ -343,17 +301,6 @@ bool staging_path_in_use(const LoadedMod& mod, const std::filesystem::path& path
|
||||
return inUse;
|
||||
}
|
||||
|
||||
std::string user_agent_version(std::string_view version) {
|
||||
std::string result{version};
|
||||
for (char& ch : result) {
|
||||
const auto value = static_cast<unsigned char>(ch);
|
||||
if (value <= 32 || value >= 127) {
|
||||
ch = '_';
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ModResult start_request(LoadedMod& mod, const HttpRequestDesc& desc, HttpCompleteFn callback,
|
||||
void* userData, HttpRequestHandle& outHandle) {
|
||||
const std::string_view url{desc.url};
|
||||
@@ -376,9 +323,7 @@ ModResult start_request(LoadedMod& mod, const HttpRequestDesc& desc, HttpComplet
|
||||
}
|
||||
const std::string_view name{header.name};
|
||||
const std::string_view value{header.value};
|
||||
const bool invalidValue = std::ranges::any_of(
|
||||
value, [](unsigned char ch) { return (ch < 32 && ch != '\t') || ch == 127; });
|
||||
if (!valid_header_name(name) || invalidValue || is_reserved_header(name) ||
|
||||
if (!valid_header(name, value, ReservedHeaders, true) ||
|
||||
name.size() > MaxHeaderBytes - headerBytes)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
@@ -450,8 +395,7 @@ ModResult start_request(LoadedMod& mod, const HttpRequestDesc& desc, HttpComplet
|
||||
}
|
||||
request.headers.push_back({
|
||||
.name = "User-Agent",
|
||||
.value = fmt::format("{}/{} {}/{}", AppName, BOREALIS_APP_VERSION, mod.metadata.id,
|
||||
user_agent_version(mod.metadata.version)),
|
||||
.value = user_agent(mod),
|
||||
});
|
||||
|
||||
auto task = borealis::http::start(std::move(request));
|
||||
@@ -487,14 +431,10 @@ ModResult http_request(ModContext* context, const HttpRequestDesc* desc, HttpCom
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (!declares_http_import(*mod)) {
|
||||
if (!declares_import(*mod, HTTP_SERVICE_ID)) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
try {
|
||||
return start_request(*mod, *desc, callback, userData, *outHandle);
|
||||
} catch (...) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
return start_request(*mod, *desc, callback, userData, *outHandle);
|
||||
}
|
||||
|
||||
ModResult http_progress(ModContext* context, HttpRequestHandle handle, HttpProgress* outProgress) {
|
||||
@@ -550,9 +490,9 @@ bool http_available() {
|
||||
|
||||
constexpr HttpService s_httpService{
|
||||
.header = SERVICE_HEADER(HttpService, HTTP_SERVICE_MAJOR, HTTP_SERVICE_MINOR),
|
||||
.request = http_request,
|
||||
.progress = http_progress,
|
||||
.cancel = http_cancel,
|
||||
.request = SERVICE_FUNCTION(http_request),
|
||||
.progress = SERVICE_FUNCTION(http_progress),
|
||||
.cancel = SERVICE_FUNCTION(http_cancel),
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "../loader/loader.hpp"
|
||||
#include "../log_buffer.hpp"
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
@@ -106,9 +111,7 @@ public:
|
||||
return erase(handle);
|
||||
}
|
||||
|
||||
size_t erase_all(const LoadedMod& owner) {
|
||||
return take_all(owner).size();
|
||||
}
|
||||
size_t erase_all(const LoadedMod& owner) { return take_all(owner).size(); }
|
||||
|
||||
template <typename Fn>
|
||||
void for_each(Fn&& fn) const {
|
||||
@@ -123,9 +126,7 @@ public:
|
||||
}
|
||||
|
||||
// Returns the index of the handle within the slot map.
|
||||
static constexpr uint32_t index_of(Handle handle) {
|
||||
return handle_index(handle);
|
||||
}
|
||||
static constexpr uint32_t index_of(Handle handle) { return handle_index(handle); }
|
||||
|
||||
private:
|
||||
struct Slot {
|
||||
@@ -189,5 +190,69 @@ private:
|
||||
std::vector<uint32_t> m_freeSlots;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class PerMod {
|
||||
public:
|
||||
template <typename... Args>
|
||||
T& get_or_create(LoadedMod& mod, Args&&... args) {
|
||||
auto& state = m_states[&mod];
|
||||
if (!state) {
|
||||
state = std::make_unique<T>(std::forward<Args>(args)...);
|
||||
}
|
||||
return *state;
|
||||
}
|
||||
|
||||
T* find(LoadedMod& mod) {
|
||||
const auto found = m_states.find(&mod);
|
||||
return found != m_states.end() ? found->second.get() : nullptr;
|
||||
}
|
||||
|
||||
const T* find(const LoadedMod& mod) const {
|
||||
const auto found = m_states.find(&mod);
|
||||
return found != m_states.end() ? found->second.get() : nullptr;
|
||||
}
|
||||
|
||||
void erase(LoadedMod& mod) { m_states.erase(&mod); }
|
||||
bool contains(const LoadedMod& mod) const { return m_states.contains(&mod); }
|
||||
void clear() { m_states.clear(); }
|
||||
|
||||
private:
|
||||
std::unordered_map<const LoadedMod*, std::unique_ptr<T>> m_states;
|
||||
};
|
||||
|
||||
template <typename Fn>
|
||||
ModResult guarded(ModContext* context, std::string_view operation, Fn&& fn) noexcept {
|
||||
try {
|
||||
return fn();
|
||||
} catch (const std::exception& exception) {
|
||||
log::emit(log::Source::Mod, mod_id_from_context(context), LOG_LEVEL_ERROR,
|
||||
fmt::format("{}: {}", operation, exception.what()));
|
||||
} catch (...) {
|
||||
log::emit(log::Source::Mod, mod_id_from_context(context), LOG_LEVEL_ERROR,
|
||||
fmt::format("{}: unknown exception", operation));
|
||||
}
|
||||
return MOD_ERROR;
|
||||
}
|
||||
|
||||
// Wraps a mod callback, attempting to catch any escaping exceptions.
|
||||
// Immediately logs and fails the mod.
|
||||
template <typename Fn>
|
||||
void guarded_callback(LoadedMod& mod, std::string_view operation, Fn&& fn) noexcept {
|
||||
try {
|
||||
fn();
|
||||
} catch (const std::exception& exception) {
|
||||
fail_mod(mod, MOD_ERROR, fmt::format("exception in {}: {}", operation, exception.what()));
|
||||
} catch (...) {
|
||||
fail_mod(mod, MOD_ERROR, fmt::format("unknown exception in {}", operation));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace svc
|
||||
} // namespace dusk::mods
|
||||
|
||||
// Wraps service functions, catching C++ exceptions to avoid unwinding into mod code.
|
||||
#define SERVICE_FUNCTION(function) \
|
||||
[](ModContext* context, auto... arguments) -> ModResult { \
|
||||
return ::dusk::mods::svc::guarded( \
|
||||
context, #function, [&] { return function(context, arguments...); }); \
|
||||
}
|
||||
@@ -0,0 +1,574 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "internal.hpp"
|
||||
#include "net.hpp"
|
||||
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/net.h"
|
||||
|
||||
#include <borealis/net.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
constexpr size_t MaxListenersPerMod = 4;
|
||||
constexpr size_t MaxStreamsPerMod = 32;
|
||||
constexpr size_t MaxDatagramsPerMod = 4;
|
||||
constexpr size_t MaxResolversPerMod = 8;
|
||||
constexpr size_t DefaultSendQueueBytes = 1024 * 1024;
|
||||
constexpr size_t MaxSendQueueBytes = 8 * 1024 * 1024;
|
||||
constexpr std::chrono::milliseconds DefaultConnectTimeout{10000};
|
||||
constexpr std::chrono::milliseconds DefaultCloseTimeout{5000};
|
||||
constexpr std::chrono::milliseconds InboundStallTimeout{30000};
|
||||
|
||||
enum class HandleKind {
|
||||
Listener,
|
||||
Stream,
|
||||
Datagram,
|
||||
Resolver,
|
||||
};
|
||||
|
||||
struct HandleState {
|
||||
borealis::net::SocketId socket = 0;
|
||||
HandleKind kind = HandleKind::Stream;
|
||||
// Stream sends remain unavailable until the mod observes CONNECTED.
|
||||
bool openPublished = false;
|
||||
};
|
||||
|
||||
struct ModState {
|
||||
ModState()
|
||||
: context{borealis::net::ContextOptions{
|
||||
.maxQueuedBytes = 16 * 1024 * 1024,
|
||||
.maxQueuedEvents = 4096,
|
||||
.maxStreams = MaxStreamsPerMod,
|
||||
.maxListeners = MaxListenersPerMod,
|
||||
.maxDatagramSockets = MaxDatagramsPerMod,
|
||||
.maxResolvers = MaxResolversPerMod,
|
||||
}} {}
|
||||
|
||||
borealis::net::Context context;
|
||||
std::unordered_map<borealis::net::SocketId, NetHandle> handles;
|
||||
borealis::net::Event currentEvent;
|
||||
};
|
||||
|
||||
SlotMap<HandleState> s_handles;
|
||||
PerMod<ModState> s_modStates;
|
||||
|
||||
NetError map_error(borealis::net::Error error) {
|
||||
switch (error) {
|
||||
case borealis::net::Error::None:
|
||||
return NET_ERROR_NONE;
|
||||
case borealis::net::Error::InvalidEndpoint:
|
||||
return NET_ERROR_INVALID_ENDPOINT;
|
||||
case borealis::net::Error::Resolve:
|
||||
return NET_ERROR_RESOLVE;
|
||||
case borealis::net::Error::Timeout:
|
||||
return NET_ERROR_TIMEOUT;
|
||||
case borealis::net::Error::Refused:
|
||||
return NET_ERROR_REFUSED;
|
||||
case borealis::net::Error::Unreachable:
|
||||
return NET_ERROR_UNREACHABLE;
|
||||
case borealis::net::Error::Reset:
|
||||
return NET_ERROR_RESET;
|
||||
case borealis::net::Error::AddressInUse:
|
||||
return NET_ERROR_ADDRESS_IN_USE;
|
||||
case borealis::net::Error::Permission:
|
||||
return NET_ERROR_PERMISSION;
|
||||
case borealis::net::Error::TooLarge:
|
||||
return NET_ERROR_TOO_LARGE;
|
||||
case borealis::net::Error::Canceled:
|
||||
return NET_ERROR_CANCELED;
|
||||
case borealis::net::Error::Network:
|
||||
default:
|
||||
return NET_ERROR_NETWORK;
|
||||
}
|
||||
}
|
||||
|
||||
ModResult map_send_result(borealis::net::SendResult result) {
|
||||
switch (result) {
|
||||
case borealis::net::SendResult::Ok:
|
||||
return MOD_OK;
|
||||
case borealis::net::SendResult::NotOpen:
|
||||
return MOD_UNAVAILABLE;
|
||||
case borealis::net::SendResult::QueueFull:
|
||||
return MOD_CONFLICT;
|
||||
case borealis::net::SendResult::TooLarge:
|
||||
case borealis::net::SendResult::InvalidEndpoint:
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return MOD_ERROR;
|
||||
}
|
||||
|
||||
NetEventType map_event(borealis::net::Event::Kind kind) {
|
||||
switch (kind) {
|
||||
case borealis::net::Event::Kind::Connected:
|
||||
return NET_EVENT_CONNECTED;
|
||||
case borealis::net::Event::Kind::Accepted:
|
||||
return NET_EVENT_ACCEPTED;
|
||||
case borealis::net::Event::Kind::StreamData:
|
||||
return NET_EVENT_STREAM_DATA;
|
||||
case borealis::net::Event::Kind::Datagram:
|
||||
return NET_EVENT_DATAGRAM;
|
||||
case borealis::net::Event::Kind::Dropped:
|
||||
return NET_EVENT_DROPPED;
|
||||
case borealis::net::Event::Kind::Resolved:
|
||||
return NET_EVENT_RESOLVED;
|
||||
case borealis::net::Event::Kind::Closed:
|
||||
return NET_EVENT_CLOSED;
|
||||
default:
|
||||
return NET_EVENT_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
bool valid_endpoint(std::string_view text, std::string_view scheme, bool requireLiteral) {
|
||||
if (text.empty() || text.size() >= NET_ENDPOINT_MAX) {
|
||||
return false;
|
||||
}
|
||||
const auto endpoint = borealis::net::parse_endpoint(text);
|
||||
return endpoint && endpoint->scheme == scheme && (!requireLiteral || endpoint->literal);
|
||||
}
|
||||
|
||||
bool copy_endpoint(NetEndpoint& output, std::string_view endpoint) {
|
||||
if (endpoint.size() >= sizeof(output.text)) {
|
||||
output.text[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
std::memcpy(output.text, endpoint.data(), endpoint.size());
|
||||
output.text[endpoint.size()] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t bounded_send_queue(size_t requested) {
|
||||
return requested != 0 ? requested : DefaultSendQueueBytes;
|
||||
}
|
||||
|
||||
std::chrono::milliseconds timeout_or_default(uint32_t value, std::chrono::milliseconds fallback) {
|
||||
return value != 0 ? std::chrono::milliseconds{value} : fallback;
|
||||
}
|
||||
|
||||
size_t count_handles(const LoadedMod& mod, HandleKind kind) {
|
||||
size_t count = 0;
|
||||
s_handles.for_each([&](NetHandle, const auto& entry) {
|
||||
if (entry.owner == &mod && entry.value.kind == kind) {
|
||||
++count;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
ModState& state_for(LoadedMod& mod) {
|
||||
return s_modStates.get_or_create(mod);
|
||||
}
|
||||
|
||||
ModState* find_state(LoadedMod& mod) {
|
||||
return s_modStates.find(mod);
|
||||
}
|
||||
|
||||
NetHandle add_handle(LoadedMod& mod, ModState& state, borealis::net::SocketId socket,
|
||||
HandleKind kind, bool openPublished) {
|
||||
const NetHandle handle = s_handles.emplace(mod, HandleState{
|
||||
.socket = socket,
|
||||
.kind = kind,
|
||||
.openPublished = openPublished,
|
||||
});
|
||||
state.handles.emplace(socket, handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
ModResult net_connect(ModContext* context, const NetConnectDesc* desc, NetHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(NetConnectDesc) ||
|
||||
desc->endpoint == nullptr || outHandle == nullptr)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (!declares_import(*mod, NET_SERVICE_ID)) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
const std::string_view endpoint{desc->endpoint};
|
||||
if (!valid_endpoint(endpoint, "tcp", false) || desc->max_send_queue_bytes > MaxSendQueueBytes) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (!borealis::net::available()) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
if (count_handles(*mod, HandleKind::Stream) >= MaxStreamsPerMod) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
auto& state = state_for(*mod);
|
||||
const size_t maxSendQueueBytes = bounded_send_queue(desc->max_send_queue_bytes);
|
||||
const borealis::net::SocketId socket = state.context.connect(endpoint,
|
||||
borealis::net::StreamOptions{
|
||||
.connectTimeout = timeout_or_default(desc->connect_timeout_ms, DefaultConnectTimeout),
|
||||
.closeTimeout = timeout_or_default(desc->close_timeout_ms, DefaultCloseTimeout),
|
||||
.inboundStallTimeout = InboundStallTimeout,
|
||||
.maxSendQueueBytes = maxSendQueueBytes,
|
||||
.readChunkBytes = 64 * 1024,
|
||||
.noDelay = desc->no_delay,
|
||||
},
|
||||
desc->user_data);
|
||||
if (socket == 0) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
*outHandle = add_handle(*mod, state, socket, HandleKind::Stream, false);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult net_listen(ModContext* context, const NetListenDesc* desc, NetHandle* outHandle,
|
||||
NetEndpoint* outLocal, NetError* outError) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
if (outLocal != nullptr) {
|
||||
*outLocal = {};
|
||||
}
|
||||
if (outError != nullptr) {
|
||||
*outError = NET_ERROR_NONE;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(NetListenDesc) ||
|
||||
desc->bind == nullptr || outHandle == nullptr || outLocal == nullptr)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (!declares_import(*mod, NET_SERVICE_ID)) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
const std::string_view bind{desc->bind};
|
||||
if (!valid_endpoint(bind, "tcp", true) || desc->max_send_queue_bytes > MaxSendQueueBytes) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (!borealis::net::available()) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
if (count_handles(*mod, HandleKind::Listener) >= MaxListenersPerMod) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
auto& state = state_for(*mod);
|
||||
const size_t maxSendQueueBytes = bounded_send_queue(desc->max_send_queue_bytes);
|
||||
const auto result = state.context.listen(bind,
|
||||
borealis::net::ListenOptions{
|
||||
.accepted =
|
||||
borealis::net::StreamOptions{
|
||||
.connectTimeout = DefaultConnectTimeout,
|
||||
.closeTimeout = timeout_or_default(desc->close_timeout_ms, DefaultCloseTimeout),
|
||||
.inboundStallTimeout = InboundStallTimeout,
|
||||
.maxSendQueueBytes = maxSendQueueBytes,
|
||||
.readChunkBytes = 64 * 1024,
|
||||
.noDelay = desc->no_delay,
|
||||
},
|
||||
.backlog = 64,
|
||||
.reuseAddress = true,
|
||||
},
|
||||
desc->user_data);
|
||||
if (result.id == 0) {
|
||||
if (outError != nullptr) {
|
||||
*outError = map_error(result.error);
|
||||
}
|
||||
DuskLog.error("[{}] could not listen on '{}': {}", mod->metadata.id, bind, result.message);
|
||||
return MOD_ERROR;
|
||||
}
|
||||
if (!copy_endpoint(*outLocal, result.localEndpoint)) {
|
||||
state.context.close(result.id);
|
||||
return MOD_ERROR;
|
||||
}
|
||||
*outHandle = add_handle(*mod, state, result.id, HandleKind::Listener, true);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult net_open_datagram(ModContext* context, const NetDatagramDesc* desc, NetHandle* outHandle,
|
||||
NetEndpoint* outLocal, NetError* outError) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
if (outLocal != nullptr) {
|
||||
*outLocal = {};
|
||||
}
|
||||
if (outError != nullptr) {
|
||||
*outError = NET_ERROR_NONE;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(NetDatagramDesc) ||
|
||||
desc->bind == nullptr || outHandle == nullptr || outLocal == nullptr)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (!declares_import(*mod, NET_SERVICE_ID)) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
const std::string_view bind{desc->bind};
|
||||
if (!valid_endpoint(bind, "udp", true) || desc->max_send_queue_bytes > MaxSendQueueBytes) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (!borealis::net::available()) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
if (count_handles(*mod, HandleKind::Datagram) >= MaxDatagramsPerMod) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
auto& state = state_for(*mod);
|
||||
const size_t maxSendQueueBytes = bounded_send_queue(desc->max_send_queue_bytes);
|
||||
const auto result = state.context.open_datagram(bind,
|
||||
borealis::net::DatagramOptions{
|
||||
.maxSendQueueBytes = maxSendQueueBytes,
|
||||
.recvBufferBytes = 1024 * 1024,
|
||||
.sendBufferBytes = 1024 * 1024,
|
||||
},
|
||||
desc->user_data);
|
||||
if (result.id == 0) {
|
||||
if (outError != nullptr) {
|
||||
*outError = map_error(result.error);
|
||||
}
|
||||
DuskLog.error("[{}] could not bind datagram socket on '{}': {}", mod->metadata.id, bind,
|
||||
result.message);
|
||||
return MOD_ERROR;
|
||||
}
|
||||
if (!copy_endpoint(*outLocal, result.localEndpoint)) {
|
||||
state.context.close(result.id);
|
||||
return MOD_ERROR;
|
||||
}
|
||||
*outHandle = add_handle(*mod, state, result.id, HandleKind::Datagram, true);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult net_resolve(
|
||||
ModContext* context, const char* endpoint, void* userData, NetHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || endpoint == nullptr || outHandle == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (!declares_import(*mod, NET_SERVICE_ID)) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
const std::string_view endpointText{endpoint};
|
||||
if (endpointText.empty() || endpointText.size() >= NET_ENDPOINT_MAX ||
|
||||
!borealis::net::parse_endpoint(endpointText))
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (!borealis::net::available()) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
if (count_handles(*mod, HandleKind::Resolver) >= MaxResolversPerMod) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
auto& state = state_for(*mod);
|
||||
const auto socket = state.context.resolve(endpointText, userData);
|
||||
if (socket == 0) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
*outHandle = add_handle(*mod, state, socket, HandleKind::Resolver, false);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult net_poll_event(ModContext* context, NetEvent* outEvent) {
|
||||
const uint32_t structSize = outEvent != nullptr ? outEvent->struct_size : 0;
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || outEvent == nullptr || structSize < sizeof(NetEvent)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
NetEvent output{.struct_size = structSize, .error_message = ""};
|
||||
auto* state = find_state(*mod);
|
||||
if (state == nullptr) {
|
||||
std::memcpy(outEvent, &output, sizeof(output));
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
while (state->context.poll(state->currentEvent)) {
|
||||
const auto handleFound = state->handles.find(state->currentEvent.id);
|
||||
if (handleFound == state->handles.end()) {
|
||||
continue;
|
||||
}
|
||||
const NetHandle handle = handleFound->second;
|
||||
auto* handleState = s_handles.find_owned(handle, *mod);
|
||||
if (handleState == nullptr) {
|
||||
continue;
|
||||
}
|
||||
output.type = map_event(state->currentEvent.kind);
|
||||
output.handle = handle;
|
||||
output.user_data = state->currentEvent.userData;
|
||||
output.data = state->currentEvent.data.data();
|
||||
output.size = state->currentEvent.data.size();
|
||||
output.dropped = state->currentEvent.dropped;
|
||||
output.error = map_error(state->currentEvent.error);
|
||||
output.error_message = state->currentEvent.message.c_str();
|
||||
if (!copy_endpoint(output.endpoint, state->currentEvent.endpoint)) {
|
||||
output.endpoint = {};
|
||||
}
|
||||
|
||||
if (state->currentEvent.kind == borealis::net::Event::Kind::Connected) {
|
||||
handleState->value.openPublished = true;
|
||||
} else if (state->currentEvent.kind == borealis::net::Event::Kind::Accepted) {
|
||||
const NetHandle accepted =
|
||||
add_handle(*mod, *state, state->currentEvent.accepted, HandleKind::Stream, true);
|
||||
output.accepted = accepted;
|
||||
} else if (state->currentEvent.kind == borealis::net::Event::Kind::Closed ||
|
||||
state->currentEvent.kind == borealis::net::Event::Kind::Resolved)
|
||||
{
|
||||
state->handles.erase(state->currentEvent.id);
|
||||
s_handles.erase(handle);
|
||||
}
|
||||
|
||||
std::memcpy(outEvent, &output, sizeof(output));
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
std::memcpy(outEvent, &output, sizeof(output));
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult net_send(ModContext* context, NetHandle handle, const void* data, size_t size) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || (size != 0 && data == nullptr)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auto* handleState = s_handles.find_owned(handle, *mod);
|
||||
auto* state = find_state(*mod);
|
||||
if (handleState == nullptr || state == nullptr ||
|
||||
handleState->value.kind != HandleKind::Stream || !handleState->value.openPublished)
|
||||
{
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
const auto* bytes = static_cast<const std::byte*>(data);
|
||||
return map_send_result(state->context.send(handleState->value.socket, {bytes, size}));
|
||||
}
|
||||
|
||||
ModResult net_send_to(
|
||||
ModContext* context, NetHandle handle, const char* endpoint, const void* data, size_t size) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || endpoint == nullptr || (size != 0 && data == nullptr)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auto* handleState = s_handles.find_owned(handle, *mod);
|
||||
auto* state = find_state(*mod);
|
||||
if (handleState == nullptr || state == nullptr ||
|
||||
handleState->value.kind != HandleKind::Datagram)
|
||||
{
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
const auto* bytes = static_cast<const std::byte*>(data);
|
||||
return map_send_result(
|
||||
state->context.send_to(handleState->value.socket, endpoint, {bytes, size}));
|
||||
}
|
||||
|
||||
ModResult net_set_user_data(ModContext* context, NetHandle handle, void* userData) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const auto* handleState = s_handles.find_owned(handle, *mod);
|
||||
auto* state = find_state(*mod);
|
||||
if (handleState == nullptr || state == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
state->context.set_user_data(handleState->value.socket, userData);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult net_stats(ModContext* context, NetHandle handle, NetStats* outStats) {
|
||||
const uint32_t structSize = outStats != nullptr ? outStats->struct_size : 0;
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || outStats == nullptr || structSize < sizeof(NetStats)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
NetStats output{.struct_size = structSize};
|
||||
const auto* handleState = s_handles.find_owned(handle, *mod);
|
||||
auto* state = find_state(*mod);
|
||||
if (handleState == nullptr || state == nullptr) {
|
||||
std::memcpy(outStats, &output, sizeof(output));
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
const auto stats = state->context.stats(handleState->value.socket);
|
||||
if (!stats) {
|
||||
std::memcpy(outStats, &output, sizeof(output));
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
output.queued_send_bytes = stats->queuedSendBytes;
|
||||
output.inbound_dropped = stats->inboundDropped;
|
||||
output.send_failures = stats->sendFailures;
|
||||
output.bytes_sent = stats->bytesSent;
|
||||
output.bytes_received = stats->bytesReceived;
|
||||
std::memcpy(outStats, &output, sizeof(output));
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult net_close(ModContext* context, NetHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auto* handleState = s_handles.find_owned(handle, *mod);
|
||||
auto* state = find_state(*mod);
|
||||
if (handleState == nullptr || state == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
state->context.close(handleState->value.socket);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void net_mod_deactivating(LoadedMod& mod) {
|
||||
(void)s_handles.take_all(mod);
|
||||
s_modStates.erase(mod);
|
||||
}
|
||||
|
||||
void net_mod_detached(LoadedMod& mod) {
|
||||
assert(find_state(mod) == nullptr);
|
||||
bool found = false;
|
||||
s_handles.for_each([&](NetHandle, const auto& entry) { found = found || entry.owner == &mod; });
|
||||
assert(!found);
|
||||
}
|
||||
|
||||
void net_shutdown() {
|
||||
s_modStates.clear();
|
||||
s_handles = {};
|
||||
}
|
||||
|
||||
bool net_available() {
|
||||
return borealis::net::available();
|
||||
}
|
||||
|
||||
constexpr NetService s_netService{
|
||||
.header = SERVICE_HEADER(NetService, NET_SERVICE_MAJOR, NET_SERVICE_MINOR),
|
||||
.connect = SERVICE_FUNCTION(net_connect),
|
||||
.listen = SERVICE_FUNCTION(net_listen),
|
||||
.open_datagram = SERVICE_FUNCTION(net_open_datagram),
|
||||
.resolve = SERVICE_FUNCTION(net_resolve),
|
||||
.poll_event = SERVICE_FUNCTION(net_poll_event),
|
||||
.send = SERVICE_FUNCTION(net_send),
|
||||
.send_to = SERVICE_FUNCTION(net_send_to),
|
||||
.set_user_data = SERVICE_FUNCTION(net_set_user_data),
|
||||
.stats = SERVICE_FUNCTION(net_stats),
|
||||
.close = SERVICE_FUNCTION(net_close),
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_netModule{
|
||||
.id = NET_SERVICE_ID,
|
||||
.majorVersion = NET_SERVICE_MAJOR,
|
||||
.minorVersion = NET_SERVICE_MINOR,
|
||||
.service = &s_netService,
|
||||
.available = net_available,
|
||||
.modDeactivating = net_mod_deactivating,
|
||||
.modDetached = net_mod_detached,
|
||||
.shutdown = net_shutdown,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
|
||||
#include "dusk/app_info.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/http.h"
|
||||
#include "mods/svc/net.h"
|
||||
#include "mods/svc/websocket.h"
|
||||
|
||||
#include <borealis/version.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
|
||||
inline bool ascii_iequals(std::string_view left, std::string_view right) {
|
||||
if (left.size() != right.size()) {
|
||||
return false;
|
||||
}
|
||||
const auto ascii_lower = [](char value) {
|
||||
return value >= 'A' && value <= 'Z' ? static_cast<char>(value - 'A' + 'a') : value;
|
||||
};
|
||||
return std::ranges::equal(
|
||||
left, right, [&](char a, char b) { return ascii_lower(a) == ascii_lower(b); });
|
||||
}
|
||||
|
||||
inline bool valid_header_name(std::string_view name) {
|
||||
constexpr std::string_view Separators{"()<>@,;:\\\"/[]?={} \t"};
|
||||
return !name.empty() && std::ranges::all_of(name, [&](unsigned char value) {
|
||||
return value > 32 && value < 127 &&
|
||||
Separators.find(static_cast<char>(value)) == std::string_view::npos;
|
||||
});
|
||||
}
|
||||
|
||||
inline bool valid_header_value(std::string_view value, bool allowHorizontalTab) {
|
||||
return std::ranges::all_of(value, [=](unsigned char character) {
|
||||
return character >= 32 || (allowHorizontalTab && character == '\t');
|
||||
}) && std::ranges::none_of(value, [](unsigned char character) { return character == 127; });
|
||||
}
|
||||
|
||||
inline bool is_reserved_header(std::string_view name, std::span<const std::string_view> reserved) {
|
||||
return std::ranges::any_of(
|
||||
reserved, [&](std::string_view value) { return ascii_iequals(name, value); });
|
||||
}
|
||||
|
||||
inline bool valid_header(std::string_view name, std::string_view value,
|
||||
std::span<const std::string_view> reserved, bool allowHorizontalTab = false) {
|
||||
return valid_header_name(name) && valid_header_value(value, allowHorizontalTab) &&
|
||||
!is_reserved_header(name, reserved);
|
||||
}
|
||||
|
||||
inline bool declares_import(const LoadedMod& mod, std::string_view serviceId) {
|
||||
return std::ranges::any_of(
|
||||
mod.manifestInfo.imports, [&](const ModManifestInfo::Import& serviceImport) {
|
||||
return serviceImport.id == serviceId;
|
||||
});
|
||||
}
|
||||
|
||||
inline bool is_network_service(std::string_view serviceId) {
|
||||
return serviceId == HTTP_SERVICE_ID || serviceId == NET_SERVICE_ID ||
|
||||
serviceId == WEBSOCKET_SERVICE_ID;
|
||||
}
|
||||
|
||||
inline std::string user_agent(const LoadedMod& mod) {
|
||||
std::string version{mod.metadata.version};
|
||||
for (char& character : version) {
|
||||
const auto value = static_cast<unsigned char>(character);
|
||||
if (value <= 32 || value >= 127) {
|
||||
character = '_';
|
||||
}
|
||||
}
|
||||
return fmt::format("{}/{} {}/{}", AppName, BOREALIS_APP_VERSION, mod.metadata.id, version);
|
||||
}
|
||||
|
||||
} // namespace dusk::mods::svc::network
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "internal.hpp"
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include <borealis/log.hpp>
|
||||
#include "JSystem/JKernel/JKRArchive.h"
|
||||
|
||||
@@ -215,6 +215,8 @@ void ModLoader::init_services() {
|
||||
&svc::g_resourceModule,
|
||||
&svc::g_fileModule,
|
||||
&svc::g_httpModule,
|
||||
&svc::g_netModule,
|
||||
&svc::g_websocketModule,
|
||||
&svc::g_hookModule,
|
||||
&svc::g_overlayModule,
|
||||
&svc::g_textureModule,
|
||||
|
||||
@@ -72,6 +72,8 @@ extern const ServiceModule g_logModule;
|
||||
extern const ServiceModule g_resourceModule;
|
||||
extern const ServiceModule g_fileModule;
|
||||
extern const ServiceModule g_httpModule;
|
||||
extern const ServiceModule g_netModule;
|
||||
extern const ServiceModule g_websocketModule;
|
||||
extern const ServiceModule g_hookModule;
|
||||
extern const ServiceModule g_overlayModule;
|
||||
extern const ServiceModule g_textureModule;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "ui.hpp"
|
||||
|
||||
#include "config.hpp"
|
||||
#include "internal.hpp"
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
#include "ui_v1.hpp"
|
||||
|
||||
#include <borealis/log.hpp>
|
||||
@@ -45,8 +45,7 @@ constexpr size_t kUiControlSelectedSize =
|
||||
offsetof(UiControlDesc, is_selected) + sizeof(UiPredicateFn);
|
||||
constexpr size_t kUiControlStringSetModeSize =
|
||||
offsetof(UiControlDesc, string_set_mode) + sizeof(UiStringSetMode);
|
||||
constexpr size_t kUiControlFilePickerSize =
|
||||
offsetof(UiControlDesc, directory_mode) + sizeof(bool);
|
||||
constexpr size_t kUiControlFilePickerSize = offsetof(UiControlDesc, directory_mode) + sizeof(bool);
|
||||
constexpr size_t kUiListItemV21Size = offsetof(UiListItem, label) + sizeof(const char*);
|
||||
constexpr size_t kUiListDescV21Size = offsetof(UiListDesc, user_data) + sizeof(void*);
|
||||
|
||||
@@ -650,8 +649,7 @@ ModResult ui_pane_add_control(
|
||||
spec.kind = ui::ModControlSpec::Kind::FilePicker;
|
||||
spec.directoryMode = desc.directory_mode;
|
||||
for (size_t i = 0; i < desc.file_filter_count; ++i) {
|
||||
spec.fileFilters.push_back(
|
||||
{desc.file_filters[i].name, desc.file_filters[i].pattern});
|
||||
spec.fileFilters.push_back({desc.file_filters[i].name, desc.file_filters[i].pattern});
|
||||
}
|
||||
break;
|
||||
case UI_CONTROL_SELECT:
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "internal.hpp"
|
||||
#include "net.hpp"
|
||||
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/websocket.h"
|
||||
|
||||
#include <borealis/url.hpp>
|
||||
#include <borealis/ws.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
constexpr size_t MaxConnectionsPerMod = 4;
|
||||
constexpr size_t MaxUrlBytes = 8 * 1024;
|
||||
constexpr size_t MaxHeaders = 64;
|
||||
constexpr size_t MaxHeaderBytes = 16 * 1024;
|
||||
constexpr size_t MaxProtocols = 8;
|
||||
constexpr size_t DefaultMessageBytes = 1024 * 1024;
|
||||
constexpr size_t MaxMessageBytes = 16 * 1024 * 1024;
|
||||
constexpr std::chrono::milliseconds DefaultConnectTimeout{10000};
|
||||
constexpr std::chrono::milliseconds DefaultCloseTimeout{5000};
|
||||
constexpr std::string_view ReservedHeaders[]{
|
||||
"User-Agent",
|
||||
"Host",
|
||||
"Connection",
|
||||
"Upgrade",
|
||||
"Sec-WebSocket-Key",
|
||||
"Sec-WebSocket-Version",
|
||||
"Sec-WebSocket-Extensions",
|
||||
"Sec-WebSocket-Protocol",
|
||||
"Content-Length",
|
||||
};
|
||||
|
||||
struct ConnectionState {
|
||||
borealis::ws::Connection connection;
|
||||
void* userData = nullptr;
|
||||
bool openPublished = false;
|
||||
};
|
||||
|
||||
struct ModPollState {
|
||||
borealis::ws::Event currentEvent;
|
||||
std::vector<HttpHeader> headers;
|
||||
size_t next = 0;
|
||||
};
|
||||
|
||||
SlotMap<ConnectionState> s_connections;
|
||||
PerMod<ModPollState> s_pollStates;
|
||||
|
||||
bool valid_url(std::string_view url, bool& allowPlaintext) {
|
||||
allowPlaintext = false;
|
||||
if (url.size() > MaxUrlBytes) {
|
||||
return false;
|
||||
}
|
||||
const auto parsed = borealis::url::parse(url);
|
||||
if (!parsed || (parsed->scheme != "wss" && parsed->scheme != "ws")) {
|
||||
return false;
|
||||
}
|
||||
allowPlaintext = parsed->scheme == "ws";
|
||||
if (!allowPlaintext) {
|
||||
return true;
|
||||
}
|
||||
return parsed->host == "localhost" || parsed->host == "127.0.0.1" || parsed->host == "::1";
|
||||
}
|
||||
|
||||
WebSocketError map_error(borealis::ws::Error error) {
|
||||
switch (error) {
|
||||
case borealis::ws::Error::None:
|
||||
return WEBSOCKET_ERROR_NONE;
|
||||
case borealis::ws::Error::InvalidUrl:
|
||||
return WEBSOCKET_ERROR_INVALID_URL;
|
||||
case borealis::ws::Error::UnsupportedScheme:
|
||||
return WEBSOCKET_ERROR_UNSUPPORTED_SCHEME;
|
||||
case borealis::ws::Error::Timeout:
|
||||
return WEBSOCKET_ERROR_TIMEOUT;
|
||||
case borealis::ws::Error::TooLarge:
|
||||
return WEBSOCKET_ERROR_TOO_LARGE;
|
||||
case borealis::ws::Error::Canceled:
|
||||
return WEBSOCKET_ERROR_CANCELED;
|
||||
case borealis::ws::Error::Protocol:
|
||||
return WEBSOCKET_ERROR_PROTOCOL;
|
||||
case borealis::ws::Error::Handshake:
|
||||
return WEBSOCKET_ERROR_HANDSHAKE;
|
||||
case borealis::ws::Error::NoBackend:
|
||||
case borealis::ws::Error::Network:
|
||||
default:
|
||||
return WEBSOCKET_ERROR_NETWORK;
|
||||
}
|
||||
}
|
||||
|
||||
ModResult map_send_result(borealis::ws::SendResult result) {
|
||||
switch (result) {
|
||||
case borealis::ws::SendResult::Ok:
|
||||
return MOD_OK;
|
||||
case borealis::ws::SendResult::NotOpen:
|
||||
return MOD_UNAVAILABLE;
|
||||
case borealis::ws::SendResult::QueueFull:
|
||||
return MOD_CONFLICT;
|
||||
case borealis::ws::SendResult::TooLarge:
|
||||
case borealis::ws::SendResult::InvalidText:
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return MOD_ERROR;
|
||||
}
|
||||
|
||||
ModResult map_close_result(borealis::ws::CloseResult result) {
|
||||
switch (result) {
|
||||
case borealis::ws::CloseResult::Ok:
|
||||
return MOD_OK;
|
||||
case borealis::ws::CloseResult::NotOpen:
|
||||
return MOD_UNAVAILABLE;
|
||||
case borealis::ws::CloseResult::InvalidCode:
|
||||
case borealis::ws::CloseResult::ReasonTooLarge:
|
||||
case borealis::ws::CloseResult::InvalidText:
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return MOD_ERROR;
|
||||
}
|
||||
|
||||
size_t connection_count(const LoadedMod& mod) {
|
||||
size_t count = 0;
|
||||
s_connections.for_each([&](WebSocketHandle, const auto& entry) {
|
||||
if (entry.owner == &mod) {
|
||||
++count;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
ModResult websocket_connect(
|
||||
ModContext* context, const WebSocketConnectDesc* desc, WebSocketHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(WebSocketConnectDesc) ||
|
||||
desc->url == nullptr || outHandle == nullptr ||
|
||||
(desc->header_count != 0 && desc->headers == nullptr) ||
|
||||
(desc->protocol_count != 0 && desc->protocols == nullptr))
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (!declares_import(*mod, WEBSOCKET_SERVICE_ID)) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
bool allowPlaintext = false;
|
||||
if (!valid_url(desc->url, allowPlaintext) || desc->header_count > MaxHeaders ||
|
||||
desc->protocol_count > MaxProtocols || desc->max_message_bytes > MaxMessageBytes)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
size_t headerBytes = 0;
|
||||
for (uint32_t index = 0; index < desc->header_count; ++index) {
|
||||
const HttpHeader& header = desc->headers[index];
|
||||
if (header.name == nullptr || header.value == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const std::string_view name{header.name};
|
||||
const std::string_view value{header.value};
|
||||
if (!valid_header(name, value, ReservedHeaders) ||
|
||||
name.size() > MaxHeaderBytes - std::min(headerBytes, MaxHeaderBytes))
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
headerBytes += name.size();
|
||||
if (value.size() > MaxHeaderBytes - std::min(headerBytes, MaxHeaderBytes)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
headerBytes += value.size();
|
||||
}
|
||||
for (uint32_t index = 0; index < desc->protocol_count; ++index) {
|
||||
if (desc->protocols[index] == nullptr || !valid_header_name(desc->protocols[index])) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
if (!borealis::ws::available()) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
if (connection_count(*mod) >= MaxConnectionsPerMod) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
|
||||
const size_t maxMessageBytes =
|
||||
desc->max_message_bytes != 0 ? desc->max_message_bytes : DefaultMessageBytes;
|
||||
borealis::ws::Options options{
|
||||
.url = desc->url,
|
||||
.connectTimeout = desc->connect_timeout_ms != 0 ?
|
||||
std::chrono::milliseconds{desc->connect_timeout_ms} :
|
||||
DefaultConnectTimeout,
|
||||
.closeTimeout = desc->close_timeout_ms != 0 ?
|
||||
std::chrono::milliseconds{desc->close_timeout_ms} :
|
||||
DefaultCloseTimeout,
|
||||
.keepaliveInterval = std::chrono::milliseconds{desc->keepalive_interval_ms},
|
||||
.maxMessageBytes = maxMessageBytes,
|
||||
.maxQueuedBytes = 16 * 1024 * 1024,
|
||||
.maxSendQueueBytes = 4 * 1024 * 1024,
|
||||
.allowPlaintext = allowPlaintext,
|
||||
};
|
||||
options.headers.reserve(desc->header_count + 1);
|
||||
for (uint32_t index = 0; index < desc->header_count; ++index) {
|
||||
options.headers.push_back({desc->headers[index].name, desc->headers[index].value});
|
||||
}
|
||||
options.headers.push_back({
|
||||
.name = "User-Agent",
|
||||
.value = user_agent(*mod),
|
||||
});
|
||||
options.protocols.reserve(desc->protocol_count);
|
||||
for (uint32_t index = 0; index < desc->protocol_count; ++index) {
|
||||
options.protocols.emplace_back(desc->protocols[index]);
|
||||
}
|
||||
auto connection = borealis::ws::connect(std::move(options));
|
||||
if (!connection) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
*outHandle = s_connections.emplace(*mod, ConnectionState{
|
||||
.connection = std::move(connection),
|
||||
.userData = desc->user_data,
|
||||
});
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult websocket_poll_event(ModContext* context, WebSocketEvent* outEvent) {
|
||||
const uint32_t structSize = outEvent != nullptr ? outEvent->struct_size : 0;
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || outEvent == nullptr || structSize < sizeof(WebSocketEvent)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
WebSocketEvent output{
|
||||
.struct_size = structSize,
|
||||
.protocol = "",
|
||||
.error_message = "",
|
||||
.close_reason = "",
|
||||
};
|
||||
|
||||
std::vector<WebSocketHandle> handles;
|
||||
s_connections.for_each([&](WebSocketHandle handle, const auto& entry) {
|
||||
if (entry.owner == mod) {
|
||||
handles.push_back(handle);
|
||||
}
|
||||
});
|
||||
auto& pollState = s_pollStates.get_or_create(*mod);
|
||||
if (handles.empty()) {
|
||||
pollState.next = 0;
|
||||
std::memcpy(outEvent, &output, sizeof(output));
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
const size_t start = pollState.next % handles.size();
|
||||
WebSocketHandle selected = 0;
|
||||
ConnectionState* selectedState = nullptr;
|
||||
for (size_t offset = 0; offset < handles.size(); ++offset) {
|
||||
const size_t index = (start + offset) % handles.size();
|
||||
auto* entry = s_connections.find_owned(handles[index], *mod);
|
||||
if (entry != nullptr && entry->value.connection.poll(pollState.currentEvent)) {
|
||||
selected = handles[index];
|
||||
selectedState = &entry->value;
|
||||
pollState.next = index + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selectedState == nullptr) {
|
||||
std::memcpy(outEvent, &output, sizeof(output));
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
output.ws = selected;
|
||||
output.user_data = selectedState->userData;
|
||||
const auto& event = pollState.currentEvent;
|
||||
pollState.headers.clear();
|
||||
pollState.headers.reserve(event.headers.size());
|
||||
for (const auto& header : event.headers) {
|
||||
pollState.headers.push_back({header.name.c_str(), header.value.c_str()});
|
||||
}
|
||||
output.headers = pollState.headers.data();
|
||||
output.header_count = static_cast<uint32_t>(pollState.headers.size());
|
||||
if (event.kind == borealis::ws::Event::Kind::Open) {
|
||||
output.type = WEBSOCKET_EVENT_OPEN;
|
||||
output.protocol = event.protocol.c_str();
|
||||
selectedState->openPublished = true;
|
||||
} else if (event.kind == borealis::ws::Event::Kind::Message) {
|
||||
output.type = WEBSOCKET_EVENT_MESSAGE;
|
||||
output.message_kind = event.messageKind == borealis::ws::MessageKind::Text ?
|
||||
WEBSOCKET_MESSAGE_TEXT :
|
||||
WEBSOCKET_MESSAGE_BINARY;
|
||||
output.data = event.data.data();
|
||||
output.size = event.data.size();
|
||||
} else {
|
||||
output.type = WEBSOCKET_EVENT_CLOSED;
|
||||
output.error = map_error(event.error);
|
||||
output.error_message = event.message.c_str();
|
||||
output.handshake_status = event.status;
|
||||
output.close_code = event.code;
|
||||
output.close_reason = event.reason.c_str();
|
||||
}
|
||||
|
||||
std::memcpy(outEvent, &output, sizeof(output));
|
||||
if (event.kind == borealis::ws::Event::Kind::Closed) {
|
||||
s_connections.erase(selected);
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult websocket_send(ModContext* context, WebSocketHandle handle, WebSocketMessageKind kind,
|
||||
const void* data, size_t size) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || (size != 0 && data == nullptr) ||
|
||||
(kind != WEBSOCKET_MESSAGE_TEXT && kind != WEBSOCKET_MESSAGE_BINARY))
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auto* entry = s_connections.find_owned(handle, *mod);
|
||||
if (entry == nullptr || !entry->value.openPublished) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
const std::string_view bytes = data != nullptr ?
|
||||
std::string_view{static_cast<const char*>(data), size} :
|
||||
std::string_view{};
|
||||
return map_send_result(entry->value.connection.send(kind == WEBSOCKET_MESSAGE_TEXT ?
|
||||
borealis::ws::MessageKind::Text :
|
||||
borealis::ws::MessageKind::Binary,
|
||||
bytes));
|
||||
}
|
||||
|
||||
ModResult websocket_close(
|
||||
ModContext* context, WebSocketHandle handle, uint16_t code, const char* reason) {
|
||||
auto* mod = mod_from_context(context);
|
||||
const std::string_view reasonText =
|
||||
reason != nullptr ? std::string_view{reason} : std::string_view{};
|
||||
if (mod == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auto* entry = s_connections.find_owned(handle, *mod);
|
||||
if (entry == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
return map_close_result(entry->value.connection.close(code != 0 ? code : 1000, reasonText));
|
||||
}
|
||||
|
||||
void websocket_mod_deactivating(LoadedMod& mod) {
|
||||
(void)s_connections.take_all(mod);
|
||||
s_pollStates.erase(mod);
|
||||
}
|
||||
|
||||
void websocket_mod_detached(LoadedMod& mod) {
|
||||
bool found = false;
|
||||
s_connections.for_each(
|
||||
[&](WebSocketHandle, const auto& entry) { found = found || entry.owner == &mod; });
|
||||
assert(!found);
|
||||
assert(!s_pollStates.contains(mod));
|
||||
}
|
||||
|
||||
void websocket_shutdown() {
|
||||
s_connections = {};
|
||||
s_pollStates.clear();
|
||||
}
|
||||
|
||||
bool websocket_available() {
|
||||
return borealis::ws::available();
|
||||
}
|
||||
|
||||
constexpr WebSocketService s_websocketService{
|
||||
.header = SERVICE_HEADER(WebSocketService, WEBSOCKET_SERVICE_MAJOR, WEBSOCKET_SERVICE_MINOR),
|
||||
.connect = SERVICE_FUNCTION(websocket_connect),
|
||||
.poll_event = SERVICE_FUNCTION(websocket_poll_event),
|
||||
.send = SERVICE_FUNCTION(websocket_send),
|
||||
.close = SERVICE_FUNCTION(websocket_close),
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_websocketModule{
|
||||
.id = WEBSOCKET_SERVICE_ID,
|
||||
.majorVersion = WEBSOCKET_SERVICE_MAJOR,
|
||||
.minorVersion = WEBSOCKET_SERVICE_MINOR,
|
||||
.service = &s_websocketService,
|
||||
.available = websocket_available,
|
||||
.modDeactivating = websocket_mod_deactivating,
|
||||
.modDetached = websocket_mod_detached,
|
||||
.shutdown = websocket_shutdown,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "window.hpp"
|
||||
|
||||
#include "internal.hpp"
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#include "mods_window.hpp"
|
||||
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "dusk/mods/svc/net.hpp"
|
||||
#include "dusk/mods/svc/ui.hpp"
|
||||
#include "fmt/format.h"
|
||||
#include "logs_window.hpp"
|
||||
#include "mod_texture_provider.hpp"
|
||||
#include "mods/svc/http.h"
|
||||
#include "pane.hpp"
|
||||
|
||||
#include "Z2AudioLib/Z2SeMgr.h"
|
||||
@@ -44,7 +44,7 @@ ModStatus mod_status(const mods::LoadedMod& mod) {
|
||||
bool mod_uses_network(const mods::LoadedMod& mod) {
|
||||
return std::ranges::any_of(
|
||||
mod.manifestInfo.imports, [](const mods::ModManifestInfo::Import& serviceImport) {
|
||||
return serviceImport.id == HTTP_SERVICE_ID;
|
||||
return mods::svc::is_network_service(serviceImport.id);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user