1
0
Fork 0
mirror of https://github.com/luanti-org/luanti.git synced 2025-06-27 16:36:03 +00:00
This commit is contained in:
DS 2025-06-27 12:35:50 +03:00 committed by GitHub
commit 71c112b8c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 1542 additions and 94 deletions

View file

@ -40,7 +40,13 @@ stds.menu_common = {
}, },
} }
files["builtin/client/register.lua"] = { files["builtin/client/init.lua"] = {
globals = {
debug = {fields={"getinfo"}},
}
}
files["builtin/sscsm_client/init.lua"] = {
globals = { globals = {
debug = {fields={"getinfo"}}, debug = {fields={"getinfo"}},
} }

View file

@ -13,3 +13,6 @@ dofile(commonpath .. "information_formspecs.lua")
dofile(clientpath .. "chatcommands.lua") dofile(clientpath .. "chatcommands.lua")
dofile(clientpath .. "misc.lua") dofile(clientpath .. "misc.lua")
assert(loadfile(commonpath .. "item_s.lua"))({}) -- Just for push/read node functions assert(loadfile(commonpath .. "item_s.lua"))({}) -- Just for push/read node functions
-- unset, as promised in initializeSecurityClient()
debug.getinfo = nil

View file

@ -1,4 +1,5 @@
local builtin_shared = ... local builtin_shared = ...
local debug_getinfo = debug.getinfo
do do
local default = {mod = "??", name = "??"} local default = {mod = "??", name = "??"}
@ -56,7 +57,7 @@ function builtin_shared.make_registration()
core.callback_origins[func] = { core.callback_origins[func] = {
-- may be nil or return nil -- may be nil or return nil
mod = core.get_current_modname and core.get_current_modname() or "??", mod = core.get_current_modname and core.get_current_modname() or "??",
name = debug.getinfo(1, "n").name or "??" name = debug_getinfo(1, "n").name or "??"
} }
end end
return t, registerfunc return t, registerfunc
@ -69,7 +70,7 @@ function builtin_shared.make_registration_reverse()
core.callback_origins[func] = { core.callback_origins[func] = {
-- may be nil or return nil -- may be nil or return nil
mod = core.get_current_modname and core.get_current_modname() or "??", mod = core.get_current_modname and core.get_current_modname() or "??",
name = debug.getinfo(1, "n").name or "??" name = debug_getinfo(1, "n").name or "??"
} }
end end
return t, registerfunc return t, registerfunc

View file

@ -1,4 +1,4 @@
local getinfo, rawget, rawset = debug.getinfo, rawget, rawset local debug_getinfo, rawget, rawset = debug.getinfo, rawget, rawset
function core.global_exists(name) function core.global_exists(name)
if type(name) ~= "string" then if type(name) ~= "string" then
@ -18,7 +18,7 @@ function meta:__newindex(name, value)
if declared[name] then if declared[name] then
return return
end end
local info = getinfo(2, "Sl") local info = debug_getinfo(2, "Sl")
if info ~= nil then if info ~= nil then
local desc = ("%s:%d"):format(info.short_src, info.currentline) local desc = ("%s:%d"):format(info.short_src, info.currentline)
local warn_key = ("%s\0%d\0%s"):format(info.source, info.currentline, name) local warn_key = ("%s\0%d\0%s"):format(info.source, info.currentline, name)
@ -36,7 +36,7 @@ function meta:__index(name)
if declared[name] then if declared[name] then
return return
end end
local info = getinfo(2, "Sl") local info = debug_getinfo(2, "Sl")
if info == nil then if info == nil then
return return
end end

View file

@ -1,5 +1,6 @@
local builtin_shared = ... local builtin_shared = ...
local S = core.get_translator("__builtin") local S = core.get_translator("__builtin")
local debug_getinfo = debug.getinfo
-- --
-- Make raw registration functions inaccessible to anyone except this file -- Make raw registration functions inaccessible to anyone except this file
@ -548,7 +549,7 @@ function core.registered_on_player_hpchange(player, hp_change, reason)
local func = core.registered_on_player_hpchanges.modifiers[i] local func = core.registered_on_player_hpchanges.modifiers[i]
hp_change, last = func(player, hp_change, reason) hp_change, last = func(player, hp_change, reason)
if type(hp_change) ~= "number" then if type(hp_change) ~= "number" then
local debuginfo = debug.getinfo(func) local debuginfo = debug_getinfo(func)
error("The register_on_hp_changes function has to return a number at " .. error("The register_on_hp_changes function has to return a number at " ..
debuginfo.short_src .. " line " .. debuginfo.linedefined) debuginfo.short_src .. " line " .. debuginfo.linedefined)
end end
@ -570,7 +571,7 @@ function core.register_on_player_hpchange(func, modifier)
end end
core.callback_origins[func] = { core.callback_origins[func] = {
mod = core.get_current_modname() or "??", mod = core.get_current_modname() or "??",
name = debug.getinfo(1, "n").name or "??" name = debug_getinfo(1, "n").name or "??"
} }
end end

View file

@ -76,6 +76,10 @@ elseif INIT == "async_game" then
dofile(asyncpath .. "game.lua") dofile(asyncpath .. "game.lua")
elseif INIT == "client" then elseif INIT == "client" then
dofile(scriptdir .. "client" .. DIR_DELIM .. "init.lua") dofile(scriptdir .. "client" .. DIR_DELIM .. "init.lua")
elseif INIT == "sscsm" and core.get_current_modname() == "*client_builtin*" then
dofile(scriptdir .. "sscsm_client" .. DIR_DELIM .. "init.lua")
elseif INIT == "sscsm" and core.get_current_modname() == "*server_builtin*" then
dofile(scriptdir .. "sscsm_server" .. DIR_DELIM .. "init.lua")
elseif INIT == "emerge" then elseif INIT == "emerge" then
dofile(scriptdir .. "emerge" .. DIR_DELIM .. "init.lua") dofile(scriptdir .. "emerge" .. DIR_DELIM .. "init.lua")
elseif INIT == "pause_menu" then elseif INIT == "pause_menu" then

View file

@ -5,6 +5,7 @@
local format, pairs, type = string.format, pairs, type local format, pairs, type = string.format, pairs, type
local core, get_current_modname = core, core.get_current_modname local core, get_current_modname = core, core.get_current_modname
local profiler, sampler, get_bool_default = ... local profiler, sampler, get_bool_default = ...
local debug_getinfo = debug.getinfo
local instrument_builtin = get_bool_default("instrument.builtin", false) local instrument_builtin = get_bool_default("instrument.builtin", false)
@ -59,7 +60,7 @@ local function generate_name(def)
local index_id = def.mod .. (class or func_name) local index_id = def.mod .. (class or func_name)
local index = counts[index_id] or 1 local index = counts[index_id] or 1
counts[index_id] = index + 1 counts[index_id] = index + 1
local info = debug.getinfo(def.func) local info = debug_getinfo(def.func)
local modpath = regex_escape(core.get_modpath(def.mod) or "") local modpath = regex_escape(core.get_modpath(def.mod) or "")
local source = info.source local source = info.source
if modpath ~= "" then if modpath ~= "" then

View file

@ -1880,6 +1880,10 @@ mgvalleys_np_dungeons (Dungeon noise) noise_params_3d 0.9, 0.5, (500, 500, 500),
# This support is experimental and API can change. # This support is experimental and API can change.
enable_client_modding (Client modding) [client] bool false enable_client_modding (Client modding) [client] bool false
# Where to enable server-sent client-side modding (SSCSM).
# Warning: Experimental.
enable_sscsm (Enable SSCSM) [client] enum nowhere nowhere,singleplayer,localhost,lan,everywhere
# Replaces the default main menu with a custom one. # Replaces the default main menu with a custom one.
main_menu_script (Main menu script) [client] string main_menu_script (Main menu script) [client] string

View file

@ -0,0 +1,25 @@
local scriptpath = core.get_builtin_path()
local commonpath = scriptpath .. "common" .. DIR_DELIM
local mypath = scriptpath .. "sscsm_client".. DIR_DELIM
-- Shared between builtin files, but
-- not exposed to outer context
local builtin_shared = {}
-- placeholders
-- FIXME: send actual content defs to sscsm env
function core.get_content_id(name)
return tonumber(name)
end
function core.get_name_from_content_id(id)
return tostring(id)
end
assert(loadfile(commonpath .. "item_s.lua"))(builtin_shared)
assert(loadfile(commonpath .. "register.lua"))(builtin_shared)
assert(loadfile(mypath .. "register.lua"))(builtin_shared)
dofile(commonpath .. "after.lua")
-- unset, as promised in initializeSecuritySSCSM()
debug.getinfo = nil

View file

@ -0,0 +1,5 @@
local builtin_shared = ...
local make_registration = builtin_shared.make_registration
core.registered_globalsteps, core.register_globalstep = make_registration()

View file

213
doc/sscsm_api.md Normal file
View file

@ -0,0 +1,213 @@
# Server-sent client-side modding (SSCSM) API reference
**Warning:** SSCSM is very experimental. The API will break. Always start your
mod with a version check (using `core.get_version()`).
In SSCSM, the server sends scripts to the client, which it executes
client-side (in a sandbox, see also `sscsm_security.md`).
As modder, you can add these scripts to your server-side mod, and tell the engine
to send them.
Please refer to `lua_api.md` for server-side modding.
(And refer to `client_lua_api.md` for client-provided client-side modding (CPCSM).)
## Loading mods
### Paths
SSCSM uses a virtual file system (just a dictionary of virtual paths (strings))
to file contents (strings).
Each mod's files have paths of the form `modname:foo/bla.lua`.
Please don't rely on this, use `core.get_modpath()` instead.
The virtual file paths within a mod are meant to mimic the filepaths on the
server, for example `<modpath>/common/foo.lua` gets sent as `modname:common/foo.lua`.
The engine loads `modname:init.lua` for all mods, in server mod dependency order.
There is client and server builtin (modnames are `*client_builtin*` and
`*server_builtin*`). The server builtin is sent from the server, like any other
SSCSM, and the client builtin is located on the client.
### Mod sending API
Currently, you can not add any mods. There's only a small hardcoded preview script
in C++ which is loaded when you set `enable_sscsm` to `singleplayer`.
## API
Unless noted otherwise, these work the same as in the server modding API.
### Global callbacks
* `core.register_globalstep(function(dtime))`
### SSCSM-specific API
* `core.get_node_or_nil(pos)`
* `core.get_content_id(name)`
* `core.get_name_from_content_id(id)`
### Util API
* `core.log([level,] text)`
* `core.get_us_time()`
* Limited in precision.
* `core.parse_json(str[, nullvalue])`
* `core.write_json(data[, styled])`
* `core.is_yes(arg)`
* `core.compress(data, method, ...)`
* `core.decompress(data, method, ...)`
* `core.encode_base64(string)`
* `core.decode_base64(string)`
* `core.get_version()`
* `core.sha1(string, raw)`
* `core.sha256(string, raw)`
* `core.colorspec_to_colorstring(colorspec)`
* `core.colorspec_to_bytes(colorspec)`
* `core.colorspec_to_table(colorspec)`
* `core.time_to_day_night_ratio(time_of_day)`
* `core.get_last_run_mod()`
* `core.set_last_run_mod(modname)`
* `core.urlencode(value)`
### Other
* `core.get_current_modname()`
* `core.get_modpath(modname)`
### Builtin helpers
* `math.*` additions
* `vector.*`
* `core.global_exists(name)`
* `core.serialize(value)`
* `core.deserialize(str, safe)`
* `dump2(obj, name, dumped)`
* `dump(obj, dumped)`
* `string.*` additions
* `table.*` additions
* `core.formspec_escape(text)`
* `core.hypertext_escape(text)`
* `core.wrap_text(str, limit, as_table)`
* `core.explode_table_event(evt)`
* `core.explode_textlist_event(evt)`
* `core.explode_scrollbar_event(evt)`
* `core.rgba(r, g, b, a)`
* `core.pos_to_string(pos, decimal_places)`
* `core.string_to_pos(value)`
* `core.string_to_area(value, relative_to)`
* `core.get_color_escape_sequence(color)`
* `core.get_background_escape_sequence(color)`
* `core.colorize(color, message)`
* `core.strip_foreground_colors(str)`
* `core.strip_background_colors(str)`
* `core.strip_colors(str)`
* `core.translate(textdomain, str, ...)`
* `core.translate_n(textdomain, str, str_plural, n, ...)`
* `core.get_translator(textdomain)`
* `core.pointed_thing_to_face_pos(placer, pointed_thing)`
* `core.string_to_privs(str, delim)`
* `core.privs_to_string(privs, delim)`
* `core.is_nan(number)`
* `core.parse_relative_number(arg, relative_to)`
* `core.parse_coordinates(x, y, z, relative_to)`
* `core.inventorycube(img1, img2, img3)`
* `core.dir_to_facedir(dir, is6d)`
* `core.facedir_to_dir(facedir)`
* `core.dir_to_fourdir(dir)`
* `core.fourdir_to_dir(fourdir)`
* `core.dir_to_wallmounted(dir)`
* `core.wallmounted_to_dir(wallmounted)`
* `core.dir_to_yaw(dir)`
* `core.yaw_to_dir(yaw)`
* `core.is_colored_paramtype(ptype)`
* `core.strip_param2_color(param2, paramtype2)`
* `core.after(time, func, ...)`
### Lua standard library
* `assert`
* `collectgarbage`
* `error`
* `getfenv`
* `ipairs`
* `next`
* `pairs`
* `pcall`
* `print`
* `rawequal`
* `rawget`
* `rawset`
* `select`
* `setfenv`
* `getmetatable`
* `setmetatable`
* `tonumber`
* `tostring`
* `type`
* `unpack`
* `_VERSION`
* `xpcall`
* `dofile`
* Overwritten.
* `load`
* Overwritten.
* `loadfile`
* Overwritten.
* `loadstring`
* Overwritten.
* `coroutine.*`
* `table.*`
* `math.*`
* `string.*`
* except `string.dump`
* `os.difftime`
* `os.time`
* `os.clock`
* Reduced precision.
* `debug.traceback`
### LuaJIT `jit` library
* `jit.arch`
* `jit.flush`
* `jit.off`
* `jit.on`
* `jit.opt`
* `jit.os`
* `jit.status`
* `jit.version`
* `jit.version_num`
### Bit library
* `bit.*`
### API only for client builtin
* `core.get_builtin_path()`
* Returns path, depending on which builtin currently loads, or `nil`.
* `debug.getinfo(...)`
* `INIT`
* Is `"sscsm"`.

76
doc/sscsm_security.md Normal file
View file

@ -0,0 +1,76 @@
# SSCSM security
## Threat model
* SSCSM scripts come from the server (potential malicious actor). We are the client.
* Authenticity of server is not given (our networking is not secure). So we have
to expect anyone who can send us UDP packets to the appropriate IP address to be
able to act on behalf of the server.
* The server may not tamper with, or get access to information of, anything besides
the stuff explicitly made accessible via the modding API (i.e. gameplay relevant
stuff, like map, node definitions, ...).
In particular, this excludes for (non-exhaustive) example files, file paths,
and settings.
* DOS is not an issue (as it is already easily possible to DOS a client).
* We already have an API via network packets (see `networkprotocol.h`).
This acts as upper bound: Every SSCSM API function could instead be a network
packet endpoint. There are no efforts to make SSCSM more secure than this.
## Non-binary `enable_sscsm` setting
The `enable_sscsm` setting does not just allow en-/disabling SSCSM, it also allows
limiting on what sort of servers to enable SSCSM. Options are `nowhere`, `singleplayer`,
`localhost` (or singleplayer), `lan` (or lower), and everywhere.
On options `localhost` and lower, we know that (anyone who acts on the behalf of)
the server runs on the same machine, and the risk of it being malicious is pretty
much zero.
Until sufficient security measures are in place, users are disallowed to set this
setting to anything higher than `localhost`.
## Lua sandbox
* We execute only Lua scripts, in a Lua sandbox.
* See also `initializeSecuritySSCSM()`.
* We do not trust the Lua implementation to not have bugs. => Additional process
isolation layer as fallback.
## Process isolation
* Not yet implemented.
* Separate SSCSM process.
* Sandboxing:
* Linux: Uses SECCOMP.
* ... (FIXME: write down stuff when you implement)
## Limit where we call into SSCSM
* Even if the Lua sandbox and/or the process isolation are bug-free, the main
process client code can still be vulnerable. Consider this example:
* Client has an inventorylist A.
* User moves an item.
* SSCSM gets called (callback when item is moved).
* SSCSM can do anything now. It decides to delete A, then returns.
* Client still has reference to A on stack, tries to access it.
* => Use-after-free.
* To avoid these sort of issues, we only give control-flow to SSCSM in few special
places.
In particular, this includes packet handlers, and the client's `step()` function.
* In these places, the client already does not assume anything about the current
state (e.g. that an inventory exists).
* This makes sure that SSCSM API calls can also just happen in these places.
In packet handlers, the server can already cause arbitrary network API "calls"
to happen. Hence, new SSCSM API calls here do not lead to new vulnerabilities
that a network API would not cause as well.
## No precise clocks
To mitigate time-based side-channel attacks, all available clock API functions
(`os.clock()` and `core.get_us_time()`) only have a precision of
`SSCSM_CLOCK_RESOLUTION_US` (20) us.

View file

@ -73,6 +73,7 @@ set(client_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/texturesource.cpp ${CMAKE_CURRENT_SOURCE_DIR}/texturesource.cpp
${CMAKE_CURRENT_SOURCE_DIR}/imagesource.cpp ${CMAKE_CURRENT_SOURCE_DIR}/imagesource.cpp
${CMAKE_CURRENT_SOURCE_DIR}/wieldmesh.cpp ${CMAKE_CURRENT_SOURCE_DIR}/wieldmesh.cpp
${CMAKE_CURRENT_SOURCE_DIR}/mod_vfs.cpp
${CMAKE_CURRENT_SOURCE_DIR}/shadows/dynamicshadows.cpp ${CMAKE_CURRENT_SOURCE_DIR}/shadows/dynamicshadows.cpp
${CMAKE_CURRENT_SOURCE_DIR}/shadows/dynamicshadowsrender.cpp ${CMAKE_CURRENT_SOURCE_DIR}/shadows/dynamicshadowsrender.cpp
${CMAKE_CURRENT_SOURCE_DIR}/shadows/shadowsshadercallbacks.cpp ${CMAKE_CURRENT_SOURCE_DIR}/shadows/shadowsshadercallbacks.cpp

View file

@ -10,6 +10,7 @@
#include <json/json.h> #include <json/json.h>
#include "client.h" #include "client.h"
#include "client/fontengine.h" #include "client/fontengine.h"
#include "client/mod_vfs.h"
#include "network/clientopcodes.h" #include "network/clientopcodes.h"
#include "network/connection.h" #include "network/connection.h"
#include "network/networkpacket.h" #include "network/networkpacket.h"
@ -54,6 +55,8 @@
#include "content/mod_configuration.h" #include "content/mod_configuration.h"
#include "mapnode.h" #include "mapnode.h"
#include "item_visuals_manager.h" #include "item_visuals_manager.h"
#include "script/sscsm/sscsm_controller.h"
#include "script/sscsm/sscsm_events.h"
extern gui::IGUIEnvironment* guienv; extern gui::IGUIEnvironment* guienv;
@ -136,6 +139,62 @@ Client::Client(
m_cache_save_interval = g_settings->getU16("server_map_save_interval"); m_cache_save_interval = g_settings->getU16("server_map_save_interval");
m_mesh_grid = { g_settings->getU16("client_mesh_chunk") }; m_mesh_grid = { g_settings->getU16("client_mesh_chunk") };
m_sscsm_controller = SSCSMController::create();
{
auto event1 = std::make_unique<SSCSMEventUpdateVFSFiles>();
ModVFS tmp_mod_vfs;
// FIXME: only read files that are relevant to sscsm, and compute sha2 digests
tmp_mod_vfs.scanModIntoMemory("*client_builtin*", getBuiltinLuaPath());
for (auto &p : tmp_mod_vfs.m_vfs) {
event1->files.emplace_back(p.first, std::move(p.second));
}
m_sscsm_controller->runEvent(this, std::move(event1));
// load client builtin immediately
auto event2 = std::make_unique<SSCSMEventLoadMods>();
event2->mods.emplace_back("*client_builtin*", "*client_builtin*:init.lua");
m_sscsm_controller->runEvent(this, std::move(event2));
}
{
//FIXME: network packets
//FIXME: check that *client_builtin* is not overridden
std::string enable_sscsm = g_settings->get("enable_sscsm");
if (enable_sscsm == "singleplayer") { //FIXME: enum
auto event1 = std::make_unique<SSCSMEventUpdateVFSFiles>();
// some simple test code
event1->files.emplace_back("sscsm_test0:init.lua",
R"=+=(
print("sscsm_test0: loading")
--print(dump(_G))
--print(debug.traceback())
do
local pos = vector.zero()
local function print_nodes()
print(string.format("node at %s: %s", pos, dump(core.get_node_or_nil(pos))))
pos = pos:offset(1, 0, 0)
core.after(1, print_nodes)
end
core.after(0, print_nodes)
end
)=+=");
m_sscsm_controller->runEvent(this, std::move(event1));
auto event2 = std::make_unique<SSCSMEventLoadMods>();
event2->mods.emplace_back("sscsm_test0", "sscsm_test0:init.lua");
m_sscsm_controller->runEvent(this, std::move(event2));
}
}
} }
void Client::migrateModStorage() void Client::migrateModStorage()
@ -183,12 +242,14 @@ void Client::loadMods()
return; return;
} }
m_mod_vfs = std::make_unique<ModVFS>();
m_script = new ClientScripting(this); m_script = new ClientScripting(this);
m_env.setScript(m_script); m_env.setScript(m_script);
m_script->setEnv(&m_env); m_script->setEnv(&m_env);
// Load builtin // Load builtin
scanModIntoMemory(BUILTIN_MOD_NAME, getBuiltinLuaPath()); m_mod_vfs->scanModIntoMemory(BUILTIN_MOD_NAME, getBuiltinLuaPath());
m_script->loadModFromMemory(BUILTIN_MOD_NAME); m_script->loadModFromMemory(BUILTIN_MOD_NAME);
m_script->checkSetByBuiltin(); m_script->checkSetByBuiltin();
@ -224,7 +285,7 @@ void Client::loadMods()
// Load "mod" scripts // Load "mod" scripts
for (const ModSpec &mod : m_mods) { for (const ModSpec &mod : m_mods) {
mod.checkAndLog(); mod.checkAndLog();
scanModIntoMemory(mod.name, mod.path); m_mod_vfs->scanModIntoMemory(mod.name, mod.path);
} }
// Run them // Run them
@ -246,35 +307,6 @@ void Client::loadMods()
m_script->on_minimap_ready(m_minimap); m_script->on_minimap_ready(m_minimap);
} }
void Client::scanModSubfolder(const std::string &mod_name, const std::string &mod_path,
std::string mod_subpath)
{
std::string full_path = mod_path + DIR_DELIM + mod_subpath;
std::vector<fs::DirListNode> mod = fs::GetDirListing(full_path);
for (const fs::DirListNode &j : mod) {
if (j.name[0] == '.')
continue;
if (j.dir) {
scanModSubfolder(mod_name, mod_path, mod_subpath + j.name + DIR_DELIM);
continue;
}
std::replace(mod_subpath.begin(), mod_subpath.end(), DIR_DELIM_CHAR, '/');
std::string real_path = full_path + j.name;
std::string vfs_path = mod_name + ":" + mod_subpath + j.name;
infostream << "Client::scanModSubfolder(): Loading \"" << real_path
<< "\" as \"" << vfs_path << "\"." << std::endl;
std::string contents;
if (!fs::ReadFile(real_path, contents, true)) {
continue;
}
m_mod_vfs.emplace(vfs_path, contents);
}
}
const std::string &Client::getBuiltinLuaPath() const std::string &Client::getBuiltinLuaPath()
{ {
static const std::string builtin_dir = porting::path_share + DIR_DELIM + "builtin"; static const std::string builtin_dir = porting::path_share + DIR_DELIM + "builtin";
@ -523,6 +555,12 @@ void Client::step(float dtime)
*/ */
LocalPlayer *player = m_env.getLocalPlayer(); LocalPlayer *player = m_env.getLocalPlayer();
{
auto event = std::make_unique<SSCSMEventOnStep>();
event->dtime = dtime;
m_sscsm_controller->runEvent(this, std::move(event));
}
// Step environment (also handles player controls) // Step environment (also handles player controls)
m_env.step(dtime); m_env.step(dtime);
m_sound->step(dtime); m_sound->step(dtime);
@ -2035,23 +2073,6 @@ scene::IAnimatedMesh* Client::getMesh(const std::string &filename, bool cache)
return mesh; return mesh;
} }
const std::string* Client::getModFile(std::string filename)
{
// strip dir delimiter from beginning of path
auto pos = filename.find_first_of(':');
if (pos == std::string::npos)
return nullptr;
pos++;
auto pos2 = filename.find_first_not_of('/', pos);
if (pos2 > pos)
filename.erase(pos, pos2 - pos);
StringMap::const_iterator it = m_mod_vfs.find(filename);
if (it == m_mod_vfs.end())
return nullptr;
return &it->second;
}
/* /*
* Mod channels * Mod channels
*/ */

View file

@ -46,6 +46,8 @@ class NodeDefManager;
class ParticleManager; class ParticleManager;
class RenderingEngine; class RenderingEngine;
class SingleMediaDownloader; class SingleMediaDownloader;
class ClientScripting;
class SSCSMController;
struct ChatMessage; struct ChatMessage;
struct ClientDynamicInfo; struct ClientDynamicInfo;
struct ClientEvent; struct ClientEvent;
@ -56,6 +58,7 @@ struct MinimapMapblock;
struct PlayerControl; struct PlayerControl;
struct PointedThing; struct PointedThing;
struct ItemVisualsManager; struct ItemVisualsManager;
struct ModVFS;
namespace con { namespace con {
class IConnection; class IConnection;
@ -99,8 +102,6 @@ private:
std::map<u16, u32> m_packets; std::map<u16, u32> m_packets;
}; };
class ClientScripting;
class Client : public con::PeerHandler, public InventoryManager, public IGameDef class Client : public con::PeerHandler, public InventoryManager, public IGameDef
{ {
public: public:
@ -126,14 +127,6 @@ public:
~Client(); ~Client();
DISABLE_CLASS_COPY(Client); DISABLE_CLASS_COPY(Client);
// Load local mods into memory
void scanModSubfolder(const std::string &mod_name, const std::string &mod_path,
std::string mod_subpath);
inline void scanModIntoMemory(const std::string &mod_name, const std::string &mod_path)
{
scanModSubfolder(mod_name, mod_path, "");
}
/* /*
request all threads managed by client to be stopped request all threads managed by client to be stopped
*/ */
@ -382,7 +375,7 @@ public:
bool checkLocalPrivilege(const std::string &priv) bool checkLocalPrivilege(const std::string &priv)
{ return checkPrivilege(priv); } { return checkPrivilege(priv); }
virtual scene::IAnimatedMesh* getMesh(const std::string &filename, bool cache = false); virtual scene::IAnimatedMesh* getMesh(const std::string &filename, bool cache = false);
const std::string* getModFile(std::string filename); ModVFS *getModVFS() { return m_mod_vfs.get(); }
ModStorageDatabase *getModStorageDatabase() override { return m_mod_storage_database; } ModStorageDatabase *getModStorageDatabase() override { return m_mod_storage_database; }
ItemVisualsManager *getItemVisualsManager() { return m_item_visuals_manager; } ItemVisualsManager *getItemVisualsManager() { return m_item_visuals_manager; }
@ -583,7 +576,10 @@ private:
ModStorageDatabase *m_mod_storage_database = nullptr; ModStorageDatabase *m_mod_storage_database = nullptr;
float m_mod_storage_save_timer = 10.0f; float m_mod_storage_save_timer = 10.0f;
std::vector<ModSpec> m_mods; std::vector<ModSpec> m_mods;
StringMap m_mod_vfs; std::unique_ptr<ModVFS> m_mod_vfs;
// SSCSM
std::unique_ptr<SSCSMController> m_sscsm_controller;
bool m_shutdown = false; bool m_shutdown = false;

56
src/client/mod_vfs.cpp Normal file
View file

@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2025 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "mod_vfs.h"
#include "filesys.h"
#include "log.h"
#include <algorithm>
void ModVFS::scanModSubfolder(const std::string &mod_name, const std::string &mod_path,
std::string mod_subpath)
{
std::string full_path = mod_path + DIR_DELIM + mod_subpath;
std::vector<fs::DirListNode> mod = fs::GetDirListing(full_path);
for (const fs::DirListNode &j : mod) {
if (j.name[0] == '.')
continue;
if (j.dir) {
scanModSubfolder(mod_name, mod_path, mod_subpath + j.name + DIR_DELIM);
continue;
}
std::replace(mod_subpath.begin(), mod_subpath.end(), DIR_DELIM_CHAR, '/');
std::string real_path = full_path + j.name;
std::string vfs_path = mod_name + ":" + mod_subpath + j.name;
infostream << "ModVFS::scanModSubfolder(): Loading \"" << real_path
<< "\" as \"" << vfs_path << "\"." << std::endl;
std::string contents;
if (!fs::ReadFile(real_path, contents)) {
errorstream << "ModVFS::scanModSubfolder(): Can't read file \""
<< real_path << "\"." << std::endl;
continue;
}
m_vfs.emplace(vfs_path, contents);
}
}
const std::string *ModVFS::getModFile(std::string filename)
{
// strip dir delimiter from beginning of path
auto pos = filename.find_first_of(':');
if (pos == std::string::npos)
return nullptr;
++pos;
auto pos2 = filename.find_first_not_of('/', pos);
if (pos2 > pos)
filename.erase(pos, pos2 - pos);
auto it = m_vfs.find(filename);
if (it == m_vfs.end())
return nullptr;
return &it->second;
}

23
src/client/mod_vfs.h Normal file
View file

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2025 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <string>
#include <unordered_map>
struct ModVFS
{
void scanModSubfolder(const std::string &mod_name, const std::string &mod_path,
std::string mod_subpath);
inline void scanModIntoMemory(const std::string &mod_name, const std::string &mod_path)
{
scanModSubfolder(mod_name, mod_path, "");
}
const std::string *getModFile(std::string filename);
std::unordered_map<std::string, std::string> m_vfs;
};

View file

@ -105,3 +105,7 @@
// The intent is to ensure that the rendering doesn't turn terribly blurry // The intent is to ensure that the rendering doesn't turn terribly blurry
// when filtering is enabled. // when filtering is enabled.
#define TEXTURE_FILTER_MIN_SIZE 192U #define TEXTURE_FILTER_MIN_SIZE 192U
// Resolution of clocks that SSCSM has access to, in us.
// Used as countermeasure against side-channel attacks.
#define SSCSM_CLOCK_RESOLUTION_US 20

View file

@ -121,6 +121,7 @@ void set_default_settings()
settings->setDefault("curl_verify_cert", "true"); settings->setDefault("curl_verify_cert", "true");
settings->setDefault("enable_remote_media_server", "true"); settings->setDefault("enable_remote_media_server", "true");
settings->setDefault("enable_client_modding", "false"); settings->setDefault("enable_client_modding", "false");
settings->setDefault("enable_sscsm", "nowhere");
settings->setDefault("max_out_chat_queue_size", "20"); settings->setDefault("max_out_chat_queue_size", "20");
settings->setDefault("pause_on_lost_focus", "false"); settings->setDefault("pause_on_lost_focus", "false");
settings->setDefault("enable_split_login_register", "true"); settings->setDefault("enable_split_login_register", "true");

View file

@ -87,6 +87,11 @@ public:
ModError(const std::string &s): BaseException(s) {} ModError(const std::string &s): BaseException(s) {}
}; };
class MisbehavedSSCSMException : public BaseException {
public:
MisbehavedSSCSMException(const std::string &s): BaseException(s) {}
};
/* /*
Some "old-style" interrupts: Some "old-style" interrupts:

View file

@ -1,6 +1,7 @@
add_subdirectory(common) add_subdirectory(common)
add_subdirectory(cpp_api) add_subdirectory(cpp_api)
add_subdirectory(lua_api) add_subdirectory(lua_api)
add_subdirectory(sscsm)
# Used by server and client # Used by server and client
set(common_SCRIPT_SRCS set(common_SCRIPT_SRCS
@ -16,8 +17,10 @@ set(client_SCRIPT_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/scripting_mainmenu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/scripting_mainmenu.cpp
${CMAKE_CURRENT_SOURCE_DIR}/scripting_client.cpp ${CMAKE_CURRENT_SOURCE_DIR}/scripting_client.cpp
${CMAKE_CURRENT_SOURCE_DIR}/scripting_pause_menu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/scripting_pause_menu.cpp
${CMAKE_CURRENT_SOURCE_DIR}/scripting_sscsm.cpp
${client_SCRIPT_COMMON_SRCS} ${client_SCRIPT_COMMON_SRCS}
${client_SCRIPT_CPP_API_SRCS} ${client_SCRIPT_CPP_API_SRCS}
${client_SCRIPT_LUA_API_SRCS} ${client_SCRIPT_LUA_API_SRCS}
${client_SCRIPT_SSCSM_SRCS}
PARENT_SCOPE) PARENT_SCOPE)

View file

@ -19,5 +19,6 @@ set(client_SCRIPT_CPP_API_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/s_client_common.cpp ${CMAKE_CURRENT_SOURCE_DIR}/s_client_common.cpp
${CMAKE_CURRENT_SOURCE_DIR}/s_mainmenu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/s_mainmenu.cpp
${CMAKE_CURRENT_SOURCE_DIR}/s_pause_menu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/s_pause_menu.cpp
${CMAKE_CURRENT_SOURCE_DIR}/s_sscsm.cpp
PARENT_SCOPE) PARENT_SCOPE)

View file

@ -16,6 +16,8 @@
#include "server.h" #include "server.h"
#if CHECK_CLIENT_BUILD() #if CHECK_CLIENT_BUILD()
#include "client/client.h" #include "client/client.h"
#include "client/mod_vfs.h"
#include "sscsm/sscsm_environment.h"
#endif #endif
#if BUILD_WITH_TRACY #if BUILD_WITH_TRACY
@ -74,7 +76,7 @@ ScriptApiBase::ScriptApiBase(ScriptingType type):
lua_atpanic(m_luastack, &luaPanic); lua_atpanic(m_luastack, &luaPanic);
if (m_type == ScriptingType::Client) if (m_type == ScriptingType::Client || m_type == ScriptingType::SSCSM)
clientOpenLibs(m_luastack); clientOpenLibs(m_luastack);
else else
luaL_openlibs(m_luastack); luaL_openlibs(m_luastack);
@ -143,7 +145,8 @@ ScriptApiBase::ScriptApiBase(ScriptingType type):
// Finally, put the table into the global environment: // Finally, put the table into the global environment:
lua_setglobal(m_luastack, "core"); lua_setglobal(m_luastack, "core");
if (m_type == ScriptingType::Client) if (m_type == ScriptingType::Client
|| m_type == ScriptingType::SSCSM)
lua_pushstring(m_luastack, "/"); lua_pushstring(m_luastack, "/");
else else
lua_pushstring(m_luastack, DIR_DELIM); lua_pushstring(m_luastack, DIR_DELIM);
@ -210,7 +213,8 @@ void ScriptApiBase::checkSetByBuiltin()
if (getType() == ScriptingType::Server || if (getType() == ScriptingType::Server ||
(getType() == ScriptingType::Async && m_gamedef) || (getType() == ScriptingType::Async && m_gamedef) ||
getType() == ScriptingType::Emerge || getType() == ScriptingType::Emerge ||
getType() == ScriptingType::Client) { getType() == ScriptingType::Client ||
getType() == ScriptingType::SSCSM) {
CHECK(CUSTOM_RIDX_READ_NODE, "read_node"); CHECK(CUSTOM_RIDX_READ_NODE, "read_node");
CHECK(CUSTOM_RIDX_PUSH_NODE, "push_node"); CHECK(CUSTOM_RIDX_PUSH_NODE, "push_node");
} }
@ -265,16 +269,18 @@ void ScriptApiBase::loadScript(const std::string &script_path)
} }
#if CHECK_CLIENT_BUILD() #if CHECK_CLIENT_BUILD()
void ScriptApiBase::loadModFromMemory(const std::string &mod_name) void ScriptApiBase::loadModFromMemory(const std::string &mod_name, std::string init_path)
{ {
ModNameStorer mod_name_storer(getStack(), mod_name); ModNameStorer mod_name_storer(getStack(), mod_name);
sanity_check(m_type == ScriptingType::Client); sanity_check(m_type == ScriptingType::Client
|| m_type == ScriptingType::SSCSM);
const std::string init_filename = mod_name + ":init.lua"; if (init_path.empty())
const std::string chunk_name = "@" + init_filename; init_path = mod_name + ":init.lua";
const std::string chunk_name = "@" + init_path;
const std::string *contents = getClient()->getModFile(init_filename); const std::string *contents = getModVFS()->getModFile(init_path);
if (!contents) if (!contents)
throw ModError("Mod \"" + mod_name + "\" lacks init.lua"); throw ModError("Mod \"" + mod_name + "\" lacks init.lua");
@ -521,8 +527,18 @@ Server* ScriptApiBase::getServer()
} }
#if CHECK_CLIENT_BUILD() #if CHECK_CLIENT_BUILD()
Client* ScriptApiBase::getClient() Client *ScriptApiBase::getClient()
{ {
return dynamic_cast<Client *>(m_gamedef); return dynamic_cast<Client *>(m_gamedef);
} }
ModVFS *ScriptApiBase::getModVFS()
{
if (m_type == ScriptingType::Client)
return getClient()->getModVFS();
else if (m_type == ScriptingType::SSCSM)
return getSSCSMEnv()->getModVFS();
else
return nullptr;
}
#endif #endif

View file

@ -43,11 +43,12 @@ extern "C" {
enum class ScriptingType: u8 { enum class ScriptingType: u8 {
Async, // either mainmenu (client) or ingame (server) Async, // either mainmenu (client) or ingame (server)
Client, Client, // CPCSM
MainMenu, MainMenu,
Server, Server,
Emerge, Emerge,
PauseMenu, PauseMenu,
SSCSM,
}; };
class Server; class Server;
@ -58,8 +59,10 @@ class EmergeThread;
class IGameDef; class IGameDef;
class Environment; class Environment;
class GUIEngine; class GUIEngine;
class SSCSMEnvironment;
class ServerActiveObject; class ServerActiveObject;
struct PlayerHPChangeReason; struct PlayerHPChangeReason;
struct ModVFS;
class ScriptApiBase : protected LuaHelper { class ScriptApiBase : protected LuaHelper {
public: public:
@ -77,7 +80,7 @@ public:
void loadScript(const std::string &script_path); void loadScript(const std::string &script_path);
#if CHECK_CLIENT_BUILD() #if CHECK_CLIENT_BUILD()
void loadModFromMemory(const std::string &mod_name); void loadModFromMemory(const std::string &mod_name, std::string init_path = "");
#endif #endif
void runCallbacksRaw(int nargs, void runCallbacksRaw(int nargs,
@ -90,9 +93,10 @@ public:
ScriptingType getType() { return m_type; } ScriptingType getType() { return m_type; }
IGameDef *getGameDef() { return m_gamedef; } IGameDef *getGameDef() { return m_gamedef; }
Server* getServer(); Server *getServer();
#if CHECK_CLIENT_BUILD() #if CHECK_CLIENT_BUILD()
Client* getClient(); Client *getClient();
ModVFS *getModVFS();
#endif #endif
// IMPORTANT: These cannot be used for any security-related uses, they exist // IMPORTANT: These cannot be used for any security-related uses, they exist
@ -158,6 +162,9 @@ protected:
#if CHECK_CLIENT_BUILD() #if CHECK_CLIENT_BUILD()
GUIEngine* getGuiEngine() { return m_guiengine; } GUIEngine* getGuiEngine() { return m_guiengine; }
void setGuiEngine(GUIEngine* guiengine) { m_guiengine = guiengine; } void setGuiEngine(GUIEngine* guiengine) { m_guiengine = guiengine; }
SSCSMEnvironment *getSSCSMEnv() { return m_sscsm_environment; }
void setSSCSMEnv(SSCSMEnvironment *env) { m_sscsm_environment = env; }
#endif #endif
EmergeThread* getEmergeThread() { return m_emerge; } EmergeThread* getEmergeThread() { return m_emerge; }
@ -184,6 +191,7 @@ private:
Environment *m_environment = nullptr; Environment *m_environment = nullptr;
#if CHECK_CLIENT_BUILD() #if CHECK_CLIENT_BUILD()
GUIEngine *m_guiengine = nullptr; GUIEngine *m_guiengine = nullptr;
SSCSMEnvironment *m_sscsm_environment = nullptr;
#endif #endif
EmergeThread *m_emerge = nullptr; EmergeThread *m_emerge = nullptr;

View file

@ -9,8 +9,10 @@
#include "server.h" #include "server.h"
#if CHECK_CLIENT_BUILD() #if CHECK_CLIENT_BUILD()
#include "client/client.h" #include "client/client.h"
#include "client/mod_vfs.h"
#endif #endif
#include "settings.h" #include "settings.h"
#include "constants.h"
#include <cerrno> #include <cerrno>
#include <string> #include <string>
@ -378,6 +380,140 @@ void ScriptApiSecurity::initializeSecurityClient()
setLuaEnv(L, thread); setLuaEnv(L, thread);
} }
void ScriptApiSecurity::initializeSecuritySSCSM()
{
static const char *whitelist[] = {
"assert",
"core",
"collectgarbage",
"DIR_DELIM",
"error",
"getfenv",
"ipairs",
"next",
"pairs",
"pcall",
"rawequal",
"rawget",
"rawset",
"select",
"setfenv",
"getmetatable",
"setmetatable",
"tonumber",
"tostring",
"type",
"unpack",
"_VERSION",
"xpcall",
// Completely safe libraries
"coroutine",
"table",
"math",
"bit",
};
static const char *os_whitelist[] = {
"difftime",
"time"
};
static const char *debug_whitelist[] = {
"getinfo", // used by client builtin and unset before mods load
"traceback"
};
static const char *string_whitelist[] = { // all but string.dump
"byte",
"char",
"dump",
"find",
"format",
"gmatch",
"gsub",
"len",
"lower",
"match",
"rep",
"reverse",
"sub",
"upper"
};
#if USE_LUAJIT
static const char *jit_whitelist[] = {
"arch",
"flush",
"off",
"on",
"opt",
"os",
"status",
"version",
"version_num",
};
#endif
m_secure = true;
lua_State *L = getStack();
int thread = getThread(L);
// create an empty environment
createEmptyEnv(L);
// Copy safe base functions
lua_getglobal(L, "_G");
lua_getfield(L, -2, "_G");
copy_safe(L, whitelist, sizeof(whitelist));
// And replace unsafe ones
SECURE_API(g, dofile);
SECURE_API(g, load);
SECURE_API(g, loadfile);
SECURE_API(g, loadstring);
SECURE_API(g, require);
lua_pop(L, 2);
// Copy safe OS functions
lua_getglobal(L, "os");
lua_newtable(L);
copy_safe(L, os_whitelist, sizeof(os_whitelist));
// And replace unsafe ones
SECURE_API(os, clock);
lua_setfield(L, -3, "os");
lua_pop(L, 1); // Pop old OS
// Copy safe debug functions
lua_getglobal(L, "debug");
lua_newtable(L);
copy_safe(L, debug_whitelist, sizeof(debug_whitelist));
lua_setfield(L, -3, "debug");
lua_pop(L, 1); // Pop old debug
// Copy safe string functions
lua_getglobal(L, "string");
lua_newtable(L);
copy_safe(L, string_whitelist, sizeof(string_whitelist));
lua_setfield(L, -3, "string");
lua_pop(L, 1); // Pop old string
#if USE_LUAJIT
// Copy safe jit functions, if they exist
lua_getglobal(L, "jit");
lua_newtable(L);
copy_safe(L, jit_whitelist, sizeof(jit_whitelist));
lua_setfield(L, -3, "jit");
lua_pop(L, 1); // Pop old jit
#endif
// Set the environment to the one we created earlier
setLuaEnv(L, thread);
}
#endif #endif
int ScriptApiSecurity::getThread(lua_State *L) int ScriptApiSecurity::getThread(lua_State *L)
@ -775,10 +911,11 @@ int ScriptApiSecurity::sl_g_loadfile(lua_State *L)
#if CHECK_CLIENT_BUILD() #if CHECK_CLIENT_BUILD()
ScriptApiBase *script = ModApiBase::getScriptApiBase(L); ScriptApiBase *script = ModApiBase::getScriptApiBase(L);
// Client implementation // SSCSM & CPCSM implementation
if (script->getType() == ScriptingType::Client) { if (script->getType() == ScriptingType::Client
|| script->getType() == ScriptingType::SSCSM) {
std::string path = readParam<std::string>(L, 1); std::string path = readParam<std::string>(L, 1);
const std::string *contents = script->getClient()->getModFile(path); const std::string *contents = script->getModVFS()->getModFile(path);
if (!contents) { if (!contents) {
std::string error_msg = "Couldn't find script called: " + path; std::string error_msg = "Couldn't find script called: " + path;
lua_pushnil(L); lua_pushnil(L);
@ -963,3 +1100,12 @@ int ScriptApiSecurity::sl_os_setlocale(lua_State *L)
lua_call(L, cat ? 2 : 1, 1); lua_call(L, cat ? 2 : 1, 1);
return 1; return 1;
} }
int ScriptApiSecurity::sl_os_clock(lua_State *L)
{
auto t = clock();
t = t - t % (SSCSM_CLOCK_RESOLUTION_US * CLOCKS_PER_SEC / 1'000'000);
lua_pushnumber(L, static_cast<lua_Number>(t) / static_cast<lua_Number>(CLOCKS_PER_SEC));
return 1;
}

View file

@ -31,8 +31,10 @@ public:
void initializeSecurity(); void initializeSecurity();
#if CHECK_CLIENT_BUILD() #if CHECK_CLIENT_BUILD()
void initializeSecurityClient(); void initializeSecurityClient();
void initializeSecuritySSCSM();
#else #else
inline void initializeSecurityClient() { assert(0); } void initializeSecurityClient() { assert(0); }
void initializeSecuritySSCSM() { assert(0); }
#endif #endif
// Checks if the Lua state has been secured // Checks if the Lua state has been secured
@ -115,4 +117,5 @@ private:
static int sl_os_rename(lua_State *L); static int sl_os_rename(lua_State *L);
static int sl_os_remove(lua_State *L); static int sl_os_remove(lua_State *L);
static int sl_os_setlocale(lua_State *L); static int sl_os_setlocale(lua_State *L);
static int sl_os_clock(lua_State *L);
}; };

View file

@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2025 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "s_sscsm.h"
#include "s_internal.h"
#include "script/sscsm/sscsm_environment.h"
void ScriptApiSSCSM::load_mods(const std::vector<std::pair<std::string, std::string>> &mods)
{
infostream << "Loading SSCSMs:" << std::endl;
for (const auto &m : mods) {
infostream << "Loading SSCSM " << m.first << std::endl;
loadModFromMemory(m.first, m.second);
}
}
void ScriptApiSSCSM::environment_step(float dtime)
{
SCRIPTAPI_PRECHECKHEADER
// Get core.registered_globalsteps
lua_getglobal(L, "core");
lua_getfield(L, -1, "registered_globalsteps");
// Call callbacks
lua_pushnumber(L, dtime);
runCallbacks(1, RUN_CALLBACKS_MODE_FIRST);
}

View file

@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2025 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "cpp_api/s_base.h"
class ScriptApiSSCSM : virtual public ScriptApiBase
{
public:
void load_mods(const std::vector<std::pair<std::string, std::string>> &mods);
void environment_step(float dtime);
};

View file

@ -39,4 +39,5 @@ set(client_SCRIPT_LUA_API_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/l_particles_local.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_particles_local.cpp
${CMAKE_CURRENT_SOURCE_DIR}/l_pause_menu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_pause_menu.cpp
${CMAKE_CURRENT_SOURCE_DIR}/l_storage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_storage.cpp
${CMAKE_CURRENT_SOURCE_DIR}/l_sscsm.cpp
PARENT_SCOPE) PARENT_SCOPE)

View file

@ -58,6 +58,11 @@ GUIEngine *ModApiBase::getGuiEngine(lua_State *L)
{ {
return getScriptApiBase(L)->getGuiEngine(); return getScriptApiBase(L)->getGuiEngine();
} }
SSCSMEnvironment *ModApiBase::getSSCSMEnv(lua_State *L)
{
return getScriptApiBase(L)->getSSCSMEnv();
}
#endif #endif
EmergeThread *ModApiBase::getEmergeThread(lua_State *L) EmergeThread *ModApiBase::getEmergeThread(lua_State *L)

View file

@ -23,6 +23,7 @@ class EmergeThread;
class ScriptApiBase; class ScriptApiBase;
class Server; class Server;
class Environment; class Environment;
class SSCSMEnvironment;
class ServerInventoryManager; class ServerInventoryManager;
class ModApiBase : protected LuaHelper { class ModApiBase : protected LuaHelper {
@ -33,6 +34,7 @@ public:
#if CHECK_CLIENT_BUILD() #if CHECK_CLIENT_BUILD()
static Client* getClient(lua_State *L); static Client* getClient(lua_State *L);
static GUIEngine* getGuiEngine(lua_State *L); static GUIEngine* getGuiEngine(lua_State *L);
static SSCSMEnvironment *getSSCSMEnv(lua_State *L);
#endif // !SERVER #endif // !SERVER
static EmergeThread* getEmergeThread(lua_State *L); static EmergeThread* getEmergeThread(lua_State *L);

View file

@ -59,7 +59,7 @@ int ModApiClient::l_get_current_modname(lua_State *L)
int ModApiClient::l_get_modpath(lua_State *L) int ModApiClient::l_get_modpath(lua_State *L)
{ {
std::string modname = readParam<std::string>(L, 1); std::string modname = readParam<std::string>(L, 1);
// Client mods use a virtual filesystem, see Client::scanModSubfolder() // Client mods use a virtual filesystem, see ModVFS::scanModSubfolder()
std::string path = modname + ":"; std::string path = modname + ":";
lua_pushstring(L, path.c_str()); lua_pushstring(L, path.c_str());
return 1; return 1;
@ -284,7 +284,20 @@ int ModApiClient::l_get_privilege_list(lua_State *L)
// get_builtin_path() // get_builtin_path()
int ModApiClient::l_get_builtin_path(lua_State *L) int ModApiClient::l_get_builtin_path(lua_State *L)
{ {
lua_pushstring(L, BUILTIN_MOD_NAME ":"); std::string modname;
if (getScriptApiBase(L)->getType() == ScriptingType::Client) {
modname = BUILTIN_MOD_NAME;
} else if (getScriptApiBase(L)->getType() == ScriptingType::SSCSM) {
// get_builtin_path() is only called in builtin, so this is fine
modname = ScriptApiBase::getCurrentModNameInsecure(L);
if (modname != "*client_builtin*" && modname != "*server_builtin*")
modname = "";
}
if (modname.empty())
return 0;
lua_pushstring(L, (modname + ":").c_str());
return 1; return 1;
} }
@ -322,3 +335,11 @@ void ModApiClient::Initialize(lua_State *L, int top)
API_FCT(get_language); API_FCT(get_language);
API_FCT(get_csm_restrictions); API_FCT(get_csm_restrictions);
} }
void ModApiClient::InitializeSSCSM(lua_State *L, int top)
{
API_FCT(get_current_modname);
API_FCT(get_modpath);
API_FCT(print);
API_FCT(get_builtin_path);
}

View file

@ -71,4 +71,5 @@ private:
public: public:
static void Initialize(lua_State *L, int top); static void Initialize(lua_State *L, int top);
static void InitializeSSCSM(lua_State *L, int top);
}; };

View file

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2025 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "l_sscsm.h"
#include "common/c_content.h"
#include "common/c_converter.h"
#include "l_internal.h"
#include "script/sscsm/sscsm_environment.h"
#include "script/sscsm/sscsm_requests.h"
// get_node_or_nil(pos)
// pos = {x=num, y=num, z=num}
int ModApiSSCSM::l_get_node_or_nil(lua_State *L)
{
// pos
v3s16 pos = read_v3s16(L, 1);
// Do it
auto request = SSCSMRequestGetNode{};
request.pos = pos;
auto answer = getSSCSMEnv(L)->doRequest(std::move(request));
if (answer.is_pos_ok) {
// Return node
pushnode(L, answer.node);
} else {
lua_pushnil(L);
}
return 1;
}
void ModApiSSCSM::Initialize(lua_State *L, int top)
{
API_FCT(get_node_or_nil);
}

View file

@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2025 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "lua_api/l_base.h"
class ModApiSSCSM : public ModApiBase
{
private:
// get_node_or_nil(pos)
static int l_get_node_or_nil(lua_State *L);
public:
static void Initialize(lua_State *L, int top);
};

View file

@ -30,6 +30,7 @@
#include "util/png.h" #include "util/png.h"
#include "player.h" #include "player.h"
#include "daynightratio.h" #include "daynightratio.h"
#include "constants.h"
#include <cstdio> #include <cstdio>
// only available in zstd 1.3.5+ // only available in zstd 1.3.5+
@ -82,6 +83,16 @@ int ModApiUtil::l_get_us_time(lua_State *L)
return 1; return 1;
} }
// get_us_time() for SSCSM
int ModApiUtil::l_get_us_time_sscsm(lua_State *L)
{
NO_MAP_LOCK_REQUIRED;
auto t = porting::getTimeUs();
t = t - t % SSCSM_CLOCK_RESOLUTION_US;
lua_pushnumber(L, t);
return 1;
}
// Maximum depth of a JSON object: // Maximum depth of a JSON object:
// Reading and writing should not overflow the Lua, C, or jsoncpp stacks. // Reading and writing should not overflow the Lua, C, or jsoncpp stacks.
constexpr static u16 MAX_JSON_DEPTH = 1024; constexpr static u16 MAX_JSON_DEPTH = 1024;
@ -785,6 +796,37 @@ void ModApiUtil::InitializeClient(lua_State *L, int top)
lua_setfield(L, top, "settings"); lua_setfield(L, top, "settings");
} }
void ModApiUtil::InitializeSSCSM(lua_State *L, int top)
{
API_FCT(log);
registerFunction(L, "get_us_time", l_get_us_time_sscsm, top);
API_FCT(parse_json);
API_FCT(write_json);
API_FCT(is_yes);
API_FCT(compress);
API_FCT(decompress);
API_FCT(encode_base64);
API_FCT(decode_base64);
API_FCT(get_version);
API_FCT(sha1);
API_FCT(sha256);
API_FCT(colorspec_to_colorstring);
API_FCT(colorspec_to_bytes);
API_FCT(colorspec_to_table);
API_FCT(time_to_day_night_ratio);
API_FCT(get_last_run_mod);
API_FCT(set_last_run_mod);
API_FCT(urlencode);
}
void ModApiUtil::InitializeAsync(lua_State *L, int top) void ModApiUtil::InitializeAsync(lua_State *L, int top)
{ {
API_FCT(log); API_FCT(log);

View file

@ -29,6 +29,9 @@ private:
// get us precision time // get us precision time
static int l_get_us_time(lua_State *L); static int l_get_us_time(lua_State *L);
// get_us_time() for SSCSM. less precise
static int l_get_us_time_sscsm(lua_State *L);
// parse_json(str[, nullvalue]) // parse_json(str[, nullvalue])
static int l_parse_json(lua_State *L); static int l_parse_json(lua_State *L);
@ -132,4 +135,5 @@ public:
static void Initialize(lua_State *L, int top); static void Initialize(lua_State *L, int top);
static void InitializeAsync(lua_State *L, int top); static void InitializeAsync(lua_State *L, int top);
static void InitializeClient(lua_State *L, int top); static void InitializeClient(lua_State *L, int top);
static void InitializeSSCSM(lua_State *L, int top);
}; };

View file

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2025 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "scripting_sscsm.h"
#include "cpp_api/s_internal.h"
#include "lua_api/l_sscsm.h"
#include "lua_api/l_util.h"
#include "lua_api/l_client.h"
SSCSMScripting::SSCSMScripting(SSCSMEnvironment *env) :
ScriptApiBase(ScriptingType::SSCSM)
{
setSSCSMEnv(env);
SCRIPTAPI_PRECHECKHEADER
initializeSecuritySSCSM();
lua_getglobal(L, "core");
int top = lua_gettop(L);
// Initialize our lua_api modules
initializeModApi(L, top);
lua_pop(L, 1);
// Push builtin initialization type
lua_pushstring(L, "sscsm");
lua_setglobal(L, "INIT");
}
void SSCSMScripting::initializeModApi(lua_State *L, int top)
{
ModApiUtil::InitializeSSCSM(L, top);
ModApiClient::InitializeSSCSM(L, top);
ModApiSSCSM::Initialize(L, top);
}

View file

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2025 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "cpp_api/s_base.h"
#include "cpp_api/s_sscsm.h"
#include "cpp_api/s_security.h"
class SSCSMScripting :
virtual public ScriptApiBase,
public ScriptApiSSCSM,
public ScriptApiSecurity
{
public:
SSCSMScripting(SSCSMEnvironment *env);
protected:
bool checkPathInternal(const std::string &abs_path, bool write_required,
bool *write_allowed) { return false; };
private:
void initializeModApi(lua_State *L, int top);
};

View file

@ -0,0 +1,4 @@
set(client_SCRIPT_SSCSM_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/sscsm_controller.cpp
${CMAKE_CURRENT_SOURCE_DIR}/sscsm_environment.cpp
PARENT_SCOPE)

View file

@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2024 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "sscsm_controller.h"
#include "sscsm_environment.h"
#include "sscsm_requests.h"
#include "sscsm_events.h"
#include "sscsm_stupid_channel.h"
std::unique_ptr<SSCSMController> SSCSMController::create()
{
auto channel = std::make_shared<StupidChannel>();
auto thread = std::make_unique<SSCSMEnvironment>(channel);
thread->start();
// Wait for thread to finish initializing.
auto req0 = deserializeSSCSMRequest(channel->recvB());
FATAL_ERROR_IF(!dynamic_cast<SSCSMRequestPollNextEvent *>(req0.get()),
"First request must be pollEvent.");
return std::make_unique<SSCSMController>(std::move(thread), channel);
}
SSCSMController::SSCSMController(std::unique_ptr<SSCSMEnvironment> thread,
std::shared_ptr<StupidChannel> channel) :
m_thread(std::move(thread)), m_channel(std::move(channel))
{
}
SSCSMController::~SSCSMController()
{
// send tear-down
auto answer = SSCSMRequestPollNextEvent::Answer{};
answer.next_event = std::make_unique<SSCSMEventTearDown>();
m_channel->sendB(serializeSSCSMAnswer(std::move(answer)));
// wait for death
m_thread->stop();
m_thread->wait();
}
SerializedSSCSMAnswer SSCSMController::handleRequest(Client *client, ISSCSMRequest *req)
{
return req->exec(client);
}
void SSCSMController::runEvent(Client *client, std::unique_ptr<ISSCSMEvent> event)
{
auto answer0 = SSCSMRequestPollNextEvent::Answer{};
answer0.next_event = std::move(event);
auto answer = serializeSSCSMAnswer(std::move(answer0));
while (true) {
auto request = deserializeSSCSMRequest(m_channel->exchangeB(std::move(answer)));
if (dynamic_cast<SSCSMRequestPollNextEvent *>(request.get()) != nullptr) {
break;
}
answer = handleRequest(client, request.get());
}
}

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2024 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <memory>
#include "irrlichttypes.h"
#include "sscsm_irequest.h"
#include "sscsm_ievent.h"
#include "util/basic_macros.h"
class SSCSMEnvironment;
class StupidChannel;
/**
* The purpose of this class is to:
* * Be the RAII owner of the SSCSM process.
* * Send events to SSCSM process, and process requests. (`runEvent`)
* * Hide details (e.g. that it is a separate process, or that it has to do IPC calls).
*
* See also SSCSMEnvironment for other side.
*/
class SSCSMController
{
std::unique_ptr<SSCSMEnvironment> m_thread;
std::shared_ptr<StupidChannel> m_channel;
SerializedSSCSMAnswer handleRequest(Client *client, ISSCSMRequest *req);
public:
static std::unique_ptr<SSCSMController> create();
SSCSMController(std::unique_ptr<SSCSMEnvironment> thread,
std::shared_ptr<StupidChannel> channel);
~SSCSMController();
DISABLE_CLASS_COPY(SSCSMController);
// Handles requests until the next event is polled
void runEvent(Client *client, std::unique_ptr<ISSCSMEvent> event);
};

View file

@ -0,0 +1,73 @@
// SPDX-FileCopyrightText: 2024 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "sscsm_environment.h"
#include "sscsm_requests.h"
#include "sscsm_events.h"
#include "sscsm_stupid_channel.h"
#include "client/mod_vfs.h"
SSCSMEnvironment::SSCSMEnvironment(std::shared_ptr<StupidChannel> channel) :
Thread("SSCSMEnvironment-thread"),
m_channel(std::move(channel)),
m_script(std::make_unique<SSCSMScripting>(this)),
m_vfs(std::make_unique<ModVFS>())
{
}
SSCSMEnvironment::~SSCSMEnvironment() = default;
void *SSCSMEnvironment::run()
{
while (true) {
auto next_event = [&]{
auto request = SSCSMRequestPollNextEvent{};
auto answer = doRequest(std::move(request));
return std::move(answer.next_event);
}();
if (dynamic_cast<SSCSMEventTearDown *>(next_event.get())) {
break;
}
try {
next_event->exec(this);
} catch (LuaError &e) {
setFatalError(std::string("Lua error: ") + e.what());
} catch (ModError &e) {
setFatalError(std::string("Mod error: ") + e.what());
}
}
return nullptr;
}
SerializedSSCSMAnswer SSCSMEnvironment::exchange(SerializedSSCSMRequest req)
{
return m_channel->exchangeA(std::move(req));
}
void SSCSMEnvironment::updateVFSFiles(std::vector<std::pair<std::string, std::string>> &&files)
{
for (auto &&p : files) {
m_vfs->m_vfs.emplace(std::move(p.first), std::move(p.second));
}
}
std::optional<std::string_view> SSCSMEnvironment::readVFSFile(const std::string &path)
{
auto it = m_vfs->m_vfs.find(path);
if (it == m_vfs->m_vfs.end())
return std::nullopt;
else
return it->second;
}
void SSCSMEnvironment::setFatalError(const std::string &reason)
{
auto request = SSCSMRequestSetFatalError{};
request.reason = reason;
doRequest(std::move(request));
}

View file

@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2024 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include "client/client.h"
#include "threading/thread.h"
#include "sscsm_controller.h"
#include "sscsm_irequest.h"
#include "../scripting_sscsm.h"
/** The thread that runs SSCSM code.
*
* Meant to be replaced by a sandboxed process.
*
* RAII-owns and abstracts away resources to communicate to the main process / thread.
*
* See also SSCSMController for other side.
*/
class SSCSMEnvironment : public Thread
{
std::shared_ptr<StupidChannel> m_channel;
std::unique_ptr<SSCSMScripting> m_script;
// the virtual file system.
// paths look like this:
// *client_builtin*:subdir/foo.lua
// *server_builtin*:subdir/foo.lua
// modname:subdir/foo.lua
std::unique_ptr<ModVFS> m_vfs;
void *run() override;
SerializedSSCSMAnswer exchange(SerializedSSCSMRequest req);
public:
SSCSMEnvironment(std::shared_ptr<StupidChannel> channel);
~SSCSMEnvironment() override;
SSCSMScripting *getScript() { return m_script.get(); }
ModVFS *getModVFS() { return m_vfs.get(); }
void updateVFSFiles(std::vector<std::pair<std::string, std::string>> &&files);
std::optional<std::string_view> readVFSFile(const std::string &path);
void setFatalError(const std::string &reason);
template <typename RQ>
typename RQ::Answer doRequest(RQ &&rq)
{
return deserializeSSCSMAnswer<typename RQ::Answer>(
exchange(serializeSSCSMRequest(std::forward<RQ>(rq)))
);
}
};

View file

@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2024 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "sscsm_ievent.h"
#include "debug.h"
#include "irrlichttypes.h"
#include "sscsm_environment.h"
struct SSCSMEventTearDown : public ISSCSMEvent
{
void exec(SSCSMEnvironment *env) override
{
FATAL_ERROR("SSCSMEventTearDown needs to be handled by SSCSMEnvironment::run()");
}
};
struct SSCSMEventUpdateVFSFiles : public ISSCSMEvent
{
// pairs are virtual path and file content
std::vector<std::pair<std::string, std::string>> files;
void exec(SSCSMEnvironment *env) override
{
env->updateVFSFiles(std::move(files));
}
};
struct SSCSMEventLoadMods : public ISSCSMEvent
{
// modnames and paths to init.lua file, in load order
std::vector<std::pair<std::string, std::string>> mods;
void exec(SSCSMEnvironment *env) override
{
env->getScript()->load_mods(mods);
}
};
struct SSCSMEventOnStep : public ISSCSMEvent
{
f32 dtime;
void exec(SSCSMEnvironment *env) override
{
env->getScript()->environment_step(dtime);
}
};

View file

@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2024 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <memory>
#include <type_traits>
class SSCSMEnvironment;
// Event triggered from the main env for the SSCSM env.
struct ISSCSMEvent
{
virtual ~ISSCSMEvent() = default;
// Note: No return value (difference to ISSCSMRequest). These are not callbacks
// that you can run at arbitrary locations, because the untrusted code could
// then clobber your local variables.
virtual void exec(SSCSMEnvironment *cntrl) = 0;
};
// FIXME: actually serialize, and replace this with a string
using SerializedSSCSMEvent = std::unique_ptr<ISSCSMEvent>;
template <typename T>
inline SerializedSSCSMEvent serializeSSCSMEvent(const T &event)
{
static_assert(std::is_base_of_v<ISSCSMEvent, T>);
return std::make_unique<T>(event);
}
inline std::unique_ptr<ISSCSMEvent> deserializeSSCSMEvent(SerializedSSCSMEvent event_serialized)
{
// The actual deserialization will have to use a type tag, and then choose
// the appropriate deserializer.
return event_serialized;
}

View file

@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: 2024 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "exceptions.h"
#include <memory>
#include <type_traits>
class SSCSMController;
class Client;
struct ISSCSMAnswer
{
virtual ~ISSCSMAnswer() = default;
};
// FIXME: actually serialize, and replace this with a string
using SerializedSSCSMAnswer = std::unique_ptr<ISSCSMAnswer>;
// Request made by the sscsm env to the main env.
struct ISSCSMRequest
{
virtual ~ISSCSMRequest() = default;
virtual SerializedSSCSMAnswer exec(Client *client) = 0;
};
// FIXME: actually serialize, and replace this with a string
using SerializedSSCSMRequest = std::unique_ptr<ISSCSMRequest>;
template <typename T>
inline SerializedSSCSMRequest serializeSSCSMRequest(const T &request)
{
static_assert(std::is_base_of_v<ISSCSMRequest, T>);
return std::make_unique<T>(request);
}
template <typename T>
inline T deserializeSSCSMAnswer(SerializedSSCSMAnswer answer_serialized)
{
static_assert(std::is_base_of_v<ISSCSMAnswer, T>);
// dynamic cast in place of actual deserialization
auto ptr = dynamic_cast<T *>(answer_serialized.get());
if (!ptr) {
throw SerializationError("deserializeSSCSMAnswer failed");
}
return std::move(*ptr);
}
template <typename T>
inline SerializedSSCSMAnswer serializeSSCSMAnswer(T &&answer)
{
static_assert(std::is_base_of_v<ISSCSMAnswer, T>);
return std::make_unique<T>(std::move(answer));
}
inline std::unique_ptr<ISSCSMRequest> deserializeSSCSMRequest(SerializedSSCSMRequest request_serialized)
{
// The actual deserialization will have to use a type tag, and then choose
// the appropriate deserializer.
return request_serialized;
}

View file

@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: 2024 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "sscsm_irequest.h"
#include "sscsm_ievent.h"
#include "mapnode.h"
#include "map.h"
#include "client/client.h"
#include "log_internal.h"
// Poll the next event (e.g. on_globalstep)
struct SSCSMRequestPollNextEvent : public ISSCSMRequest
{
struct Answer : public ISSCSMAnswer
{
std::unique_ptr<ISSCSMEvent> next_event;
};
SerializedSSCSMAnswer exec(Client *client) override
{
FATAL_ERROR("SSCSMRequestPollNextEvent needs to be handled by SSCSMControler::runEvent()");
}
};
// Some error occured in the SSCSM env
struct SSCSMRequestSetFatalError : public ISSCSMRequest
{
struct Answer : public ISSCSMAnswer
{
};
std::string reason;
SerializedSSCSMAnswer exec(Client *client) override
{
client->setFatalError("[SSCSM] " + reason);
return serializeSSCSMAnswer(Answer{});
}
};
// print(text)
// FIXME: override global loggers to use this in sscsm process
struct SSCSMRequestPrint : public ISSCSMRequest
{
struct Answer : public ISSCSMAnswer
{
};
std::string text;
SerializedSSCSMAnswer exec(Client *client) override
{
rawstream << text << std::endl;
return serializeSSCSMAnswer(Answer{});
}
};
// core.log(level, text)
// FIXME: override global loggers to use this in sscsm process
struct SSCSMRequestLog : public ISSCSMRequest
{
struct Answer : public ISSCSMAnswer
{
};
std::string text;
LogLevel level;
SerializedSSCSMAnswer exec(Client *client) override
{
if (level >= LL_MAX) {
throw MisbehavedSSCSMException("Tried to log at non-existent level.");
} else {
g_logger.log(level, text);
}
return serializeSSCSMAnswer(Answer{});
}
};
// core.get_node(pos)
struct SSCSMRequestGetNode : public ISSCSMRequest
{
struct Answer : public ISSCSMAnswer
{
MapNode node;
bool is_pos_ok;
};
v3s16 pos;
SerializedSSCSMAnswer exec(Client *client) override
{
bool is_pos_ok = false;
MapNode node = client->getEnv().getMap().getNode(pos, &is_pos_ok);
Answer answer{};
answer.node = node;
answer.is_pos_ok = is_pos_ok;
return serializeSSCSMAnswer(std::move(answer));
}
};

View file

@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: 2024 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <memory>
#include <mutex>
#include <condition_variable>
#include "sscsm_irequest.h"
// FIXME: replace this with an ipc channel
class StupidChannel
{
std::mutex m_mutex;
std::condition_variable m_condvar;
SerializedSSCSMRequest m_request;
SerializedSSCSMAnswer m_answer;
public:
void sendA(SerializedSSCSMRequest request)
{
{
auto lock = std::lock_guard(m_mutex);
m_request = std::move(request);
}
m_condvar.notify_one();
}
SerializedSSCSMAnswer recvA()
{
auto lock = std::unique_lock(m_mutex);
while (!m_answer) {
m_condvar.wait(lock);
}
auto answer = std::move(m_answer);
m_answer = nullptr;
return answer;
}
SerializedSSCSMAnswer exchangeA(SerializedSSCSMRequest request)
{
sendA(std::move(request));
return recvA();
}
void sendB(SerializedSSCSMAnswer answer)
{
{
auto lock = std::lock_guard(m_mutex);
m_answer = std::move(answer);
}
m_condvar.notify_one();
}
SerializedSSCSMRequest recvB()
{
auto lock = std::unique_lock(m_mutex);
while (!m_request) {
m_condvar.wait(lock);
}
auto request = std::move(m_request);
m_request = nullptr;
return request;
}
SerializedSSCSMRequest exchangeB(SerializedSSCSMAnswer answer)
{
sendB(std::move(answer));
return recvB();
}
};