update Android to use shared mosis-core library
- Remove duplicate sandbox sources from Android (now in core/) - Update Android CMakeLists to link mosis-core - Add OpenSSL crypto support for Android - Update all includes to use core library headers - Remove duplicate logger from Android (use core logger) - Add openssl to Android vcpkg dependencies This removes ~5,500 lines of duplicate code by sharing the sandbox implementation between desktop and Android. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,17 @@ else()
|
||||
target_compile_definitions(mosis-core PUBLIC MOSIS_PLATFORM_LINUX)
|
||||
endif()
|
||||
|
||||
# OpenSSL for crypto on non-Windows platforms
|
||||
if(NOT WIN32)
|
||||
find_package(OpenSSL QUIET)
|
||||
if(OpenSSL_FOUND)
|
||||
target_link_libraries(mosis-core PRIVATE OpenSSL::Crypto)
|
||||
target_compile_definitions(mosis-core PRIVATE MOSIS_HAS_OPENSSL)
|
||||
else()
|
||||
message(WARNING "OpenSSL not found - crypto functions will be stubs")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Lua is required - parent project must provide it
|
||||
if(TARGET lua_static)
|
||||
target_link_libraries(mosis-core PUBLIC lua_static)
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
#include <windows.h>
|
||||
#include <bcrypt.h>
|
||||
#pragma comment(lib, "bcrypt.lib")
|
||||
#elif defined(MOSIS_HAS_OPENSSL)
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/hmac.h>
|
||||
#endif
|
||||
|
||||
namespace mosis {
|
||||
@@ -162,9 +165,68 @@ std::string ComputeHMAC(HashAlgorithm algo, const std::string& key, const std::s
|
||||
return result;
|
||||
}
|
||||
|
||||
#elif defined(MOSIS_HAS_OPENSSL)
|
||||
|
||||
//=============================================================================
|
||||
// HASHING (OpenSSL)
|
||||
//=============================================================================
|
||||
|
||||
static std::string BytesToHex(const unsigned char* data, size_t len) {
|
||||
std::ostringstream oss;
|
||||
oss << std::hex << std::setfill('0');
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
oss << std::setw(2) << static_cast<int>(data[i]);
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
static const EVP_MD* GetOpenSSLAlgorithm(HashAlgorithm algo) {
|
||||
switch (algo) {
|
||||
case HashAlgorithm::SHA256: return EVP_sha256();
|
||||
case HashAlgorithm::SHA512: return EVP_sha512();
|
||||
case HashAlgorithm::SHA1: return EVP_sha1();
|
||||
case HashAlgorithm::MD5: return EVP_md5();
|
||||
default: return EVP_sha256();
|
||||
}
|
||||
}
|
||||
|
||||
std::string ComputeHash(HashAlgorithm algo, const std::string& data) {
|
||||
const EVP_MD* md = GetOpenSSLAlgorithm(algo);
|
||||
unsigned char hash[EVP_MAX_MD_SIZE];
|
||||
unsigned int hash_len = 0;
|
||||
|
||||
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
|
||||
if (!ctx) return "";
|
||||
|
||||
if (EVP_DigestInit_ex(ctx, md, nullptr) != 1 ||
|
||||
EVP_DigestUpdate(ctx, data.data(), data.size()) != 1 ||
|
||||
EVP_DigestFinal_ex(ctx, hash, &hash_len) != 1) {
|
||||
EVP_MD_CTX_free(ctx);
|
||||
return "";
|
||||
}
|
||||
|
||||
EVP_MD_CTX_free(ctx);
|
||||
return BytesToHex(hash, hash_len);
|
||||
}
|
||||
|
||||
std::string ComputeHMAC(HashAlgorithm algo, const std::string& key, const std::string& data) {
|
||||
const EVP_MD* md = GetOpenSSLAlgorithm(algo);
|
||||
unsigned char hmac_result[EVP_MAX_MD_SIZE];
|
||||
unsigned int hmac_len = 0;
|
||||
|
||||
unsigned char* result = HMAC(md,
|
||||
key.data(), static_cast<int>(key.size()),
|
||||
reinterpret_cast<const unsigned char*>(data.data()),
|
||||
data.size(),
|
||||
hmac_result, &hmac_len);
|
||||
|
||||
if (!result) return "";
|
||||
return BytesToHex(hmac_result, hmac_len);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
// Stub implementations for non-Windows (would need OpenSSL or similar)
|
||||
// Stub implementations when no crypto library is available
|
||||
std::string ComputeHash(HashAlgorithm algo, const std::string& data) {
|
||||
(void)algo;
|
||||
(void)data;
|
||||
|
||||
@@ -11,10 +11,11 @@ set(AIDL_EXE "${ANDROID_SDK}/build-tools/36.1.0/aidl.exe")
|
||||
# Find Lua from vcpkg
|
||||
find_package(Lua REQUIRED)
|
||||
|
||||
# Find nlohmann_json, minizip and sqlite3 for app management and sandbox
|
||||
# Find nlohmann_json, minizip, sqlite3 and openssl for app management and sandbox
|
||||
find_package(nlohmann_json CONFIG REQUIRED)
|
||||
find_package(minizip CONFIG REQUIRED)
|
||||
find_package(unofficial-sqlite3 CONFIG REQUIRED)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
# Fetch RmlUi from GitHub with Lua bindings enabled
|
||||
include(FetchContent)
|
||||
@@ -31,12 +32,30 @@ set(RMLUI_FONT_ENGINE "freetype" CACHE STRING "" FORCE)
|
||||
set(RMLUI_PRECOMPILED_HEADERS OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_MakeAvailable(rmlui)
|
||||
|
||||
#get_cmake_property(_variableNames VARIABLES)
|
||||
#list(SORT _variableNames)
|
||||
#foreach(_variableName ${_variableNames})
|
||||
# message(STATUS "${_variableName}=${${_variableName}}")
|
||||
#endforeach()
|
||||
#==============================================================================
|
||||
# Mosis Core Library (shared sandbox APIs)
|
||||
#==============================================================================
|
||||
# Enable optional features for Android
|
||||
set(MOSIS_ENABLE_DATABASE ON CACHE BOOL "" FORCE)
|
||||
set(MOSIS_ENABLE_NETWORK ON CACHE BOOL "" FORCE)
|
||||
|
||||
# Add core library from project root
|
||||
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../../core ${CMAKE_CURRENT_BINARY_DIR}/mosis-core)
|
||||
|
||||
# Configure core library for Android
|
||||
target_compile_definitions(mosis-core PUBLIC MOSIS_PLATFORM_ANDROID)
|
||||
target_compile_definitions(mosis-core PRIVATE MOSIS_HAS_OPENSSL)
|
||||
target_include_directories(mosis-core PUBLIC ${LUA_INCLUDE_DIR})
|
||||
target_link_libraries(mosis-core PUBLIC
|
||||
${LUA_LIBRARIES}
|
||||
nlohmann_json::nlohmann_json
|
||||
unofficial::sqlite3::sqlite3
|
||||
OpenSSL::Crypto
|
||||
)
|
||||
|
||||
#==============================================================================
|
||||
# AIDL Generated Files
|
||||
#==============================================================================
|
||||
add_custom_command(
|
||||
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/com/omixlab/mosis/IMosisService.cpp"
|
||||
COMMAND ${AIDL_EXE} --lang=ndk --min_sdk_version=36 -o ${CMAKE_CURRENT_BINARY_DIR} -h ${CMAKE_CURRENT_BINARY_DIR} -I ..\\aidl ..\\aidl\\com\\omixlab\\mosis\\IMosisService.aidl
|
||||
@@ -53,36 +72,38 @@ add_custom_command(
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
#==============================================================================
|
||||
# Mosis Service (Android)
|
||||
#==============================================================================
|
||||
add_library(mosis-service SHARED
|
||||
# Entry point and core
|
||||
mosis-service.cpp
|
||||
kernel.cpp
|
||||
|
||||
# AIDL generated
|
||||
${CMAKE_CURRENT_BINARY_DIR}/com/omixlab/mosis/IMosisService.cpp
|
||||
${CMAKE_CURRENT_BINARY_DIR}/com/omixlab/mosis/IMosisListener.cpp
|
||||
|
||||
# Platform-specific utilities
|
||||
assets_manager.cpp
|
||||
shader.cpp
|
||||
external_texture.cpp
|
||||
quad.cpp
|
||||
egl_context.cpp
|
||||
render_target.cpp
|
||||
logger.cpp
|
||||
kernel.cpp
|
||||
|
||||
# OpenGL ES
|
||||
glad/src/egl.c
|
||||
glad/src/gles2.c
|
||||
RmlUi_Renderer_GL3.cpp
|
||||
|
||||
# App management (uses core + Android-specific features)
|
||||
apps/app_manager.cpp
|
||||
apps/app_api.cpp
|
||||
apps/update_service.cpp
|
||||
sandbox/lua_sandbox.cpp
|
||||
sandbox/permission_gate.cpp
|
||||
sandbox/audit_log.cpp
|
||||
sandbox/rate_limiter.cpp
|
||||
sandbox/path_sandbox.cpp
|
||||
sandbox/timer_manager.cpp
|
||||
sandbox/json_api.cpp
|
||||
sandbox/crypto_api.cpp
|
||||
sandbox/virtual_fs.cpp
|
||||
sandbox/database_manager.cpp
|
||||
sandbox/http_validator.cpp
|
||||
sandbox/network_manager.cpp
|
||||
|
||||
# Android-specific sandbox components (hardware interfaces)
|
||||
sandbox/sandbox_manager.cpp
|
||||
sandbox/websocket_manager.cpp
|
||||
sandbox/camera_interface.cpp
|
||||
sandbox/microphone_interface.cpp
|
||||
@@ -92,27 +113,41 @@ add_library(mosis-service SHARED
|
||||
sandbox/bluetooth_interface.cpp
|
||||
sandbox/contacts_interface.cpp
|
||||
sandbox/message_bus.cpp
|
||||
sandbox/sandbox_manager.cpp
|
||||
)
|
||||
|
||||
target_compile_definitions(mosis-service PUBLIC
|
||||
RMLUI_NUM_MSAA_SAMPLES=2
|
||||
RMLUI_GL3_CUSTOM_LOADER=<glad/gles2.h>
|
||||
MOSIS_PLATFORM_ANDROID
|
||||
)
|
||||
|
||||
target_include_directories(mosis-service PUBLIC
|
||||
${CMAKE_CURRENT_LIST_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
${BINDER_DIR}
|
||||
glad/include
|
||||
# Core library headers
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../../core/include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../../core/include/mosis/sandbox
|
||||
)
|
||||
|
||||
target_link_libraries(mosis-service
|
||||
# Core library (provides sandbox APIs)
|
||||
mosis-core
|
||||
# Android system libraries
|
||||
android log binder_ndk EGL GLESv2 nativewindow
|
||||
# RmlUi
|
||||
rmlui rmlui_lua
|
||||
# Dependencies
|
||||
nlohmann_json::nlohmann_json
|
||||
minizip::minizip
|
||||
unofficial::sqlite3::sqlite3
|
||||
${LUA_LIBRARIES}
|
||||
)
|
||||
|
||||
#==============================================================================
|
||||
# Mosis Test Library
|
||||
#==============================================================================
|
||||
add_library(mosis-test SHARED
|
||||
${CMAKE_CURRENT_BINARY_DIR}/com/omixlab/mosis/IMosisService.cpp
|
||||
${CMAKE_CURRENT_BINARY_DIR}/com/omixlab/mosis/IMosisListener.cpp
|
||||
@@ -123,16 +158,20 @@ add_library(mosis-test SHARED
|
||||
quad.cpp
|
||||
egl_context.cpp
|
||||
render_target.cpp
|
||||
logger.cpp
|
||||
glad/src/egl.c
|
||||
glad/src/gles2.c
|
||||
)
|
||||
|
||||
target_include_directories(mosis-test PUBLIC
|
||||
${CMAKE_CURRENT_LIST_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
${BINDER_DIR}
|
||||
glad/include
|
||||
# Core library includes
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../../core/include
|
||||
)
|
||||
|
||||
target_link_libraries(mosis-test
|
||||
mosis-core
|
||||
android log binder_ndk EGL GLESv2 nativewindow
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "egl_context.h"
|
||||
#include "logger.h"
|
||||
#include <mosis/util/logger.h>
|
||||
#include <glad/gles2.h>
|
||||
#include <glad/egl.h>
|
||||
#include <android/native_window_jni.h>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "external_texture.h"
|
||||
#include <android/hardware_buffer.h>
|
||||
#include <glad/egl.h>
|
||||
#include "logger.h"
|
||||
#include <mosis/util/logger.h>
|
||||
|
||||
bool ExternalTexture::create(AHardwareBuffer *hardwareBuffer)
|
||||
{
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
#include "logger.h"
|
||||
#include <android/log.h>
|
||||
#define LOG_TAG "MosisOS"
|
||||
#include <android/log_macros.h>
|
||||
|
||||
void Logger::Log(const std::string &message)
|
||||
{
|
||||
ALOGI("%s", message.c_str());
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <cstdio>
|
||||
#include <cstdarg>
|
||||
|
||||
class Logger
|
||||
{
|
||||
public:
|
||||
static void Log(const std::string& message);
|
||||
|
||||
// Printf-style logging
|
||||
static void LogF(const char* level, const char* fmt, ...) {
|
||||
char buffer[1024];
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
vsnprintf(buffer, sizeof(buffer), fmt, args);
|
||||
va_end(args);
|
||||
Log(std::string("[") + level + "] " + buffer);
|
||||
}
|
||||
};
|
||||
|
||||
// Undefine conflicting syslog macros if present
|
||||
#ifdef LOG_DEBUG
|
||||
#undef LOG_DEBUG
|
||||
#endif
|
||||
#ifdef LOG_INFO
|
||||
#undef LOG_INFO
|
||||
#endif
|
||||
#ifdef LOG_WARN
|
||||
#undef LOG_WARN
|
||||
#endif
|
||||
#ifdef LOG_ERROR
|
||||
#undef LOG_ERROR
|
||||
#endif
|
||||
|
||||
// Logging macros for convenience (printf-style)
|
||||
#define LOG_DEBUG(fmt, ...) Logger::LogF("DEBUG", fmt __VA_OPT__(,) __VA_ARGS__)
|
||||
#define LOG_INFO(fmt, ...) Logger::LogF("INFO", fmt __VA_OPT__(,) __VA_ARGS__)
|
||||
#define LOG_WARN(fmt, ...) Logger::LogF("WARN", fmt __VA_OPT__(,) __VA_ARGS__)
|
||||
#define LOG_ERROR(fmt, ...) Logger::LogF("ERROR", fmt __VA_OPT__(,) __VA_ARGS__)
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <aidl/com/omixlab/mosis/BnMosisService.h>
|
||||
#include <aidl/com/omixlab/mosis/IMosisListener.h>
|
||||
#include <aidl/com/omixlab/mosis/BnMosisListener.h>
|
||||
#include "logger.h"
|
||||
#include <mosis/util/logger.h>
|
||||
#include "kernel.h"
|
||||
#include "assets_manager.h"
|
||||
#include <glad/egl.h>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include <android/native_window_jni.h>
|
||||
#include <android/asset_manager.h>
|
||||
#include <android/asset_manager_jni.h>
|
||||
#include "logger.h"
|
||||
#include <mosis/util/logger.h>
|
||||
#include "egl_context.h"
|
||||
#include "assets_manager.h"
|
||||
#include "shader.h"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "render_target.h"
|
||||
#include <android/hardware_buffer.h>
|
||||
#include <glad/egl.h>
|
||||
#include "logger.h"
|
||||
#include <mosis/util/logger.h>
|
||||
bool RenderTarget::create(uint32_t width, uint32_t height)
|
||||
{
|
||||
glGenFramebuffers(1, &m_framebuffer);
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
#include "audit_log.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace mosis {
|
||||
|
||||
//=============================================================================
|
||||
// CONSTRUCTOR
|
||||
//=============================================================================
|
||||
|
||||
AuditLog::AuditLog(size_t max_entries)
|
||||
: m_max_entries(max_entries)
|
||||
{
|
||||
m_entries.resize(max_entries);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// LOGGING
|
||||
//=============================================================================
|
||||
|
||||
void AuditLog::Log(AuditEvent event, const std::string& app_id,
|
||||
const std::string& details, bool success) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
AuditEntry entry{
|
||||
.timestamp = std::chrono::system_clock::now(),
|
||||
.event = event,
|
||||
.app_id = app_id,
|
||||
.details = details,
|
||||
.success = success
|
||||
};
|
||||
|
||||
m_entries[m_write_index] = std::move(entry);
|
||||
m_write_index = (m_write_index + 1) % m_max_entries;
|
||||
m_total_logged++;
|
||||
|
||||
if (m_total_logged > m_max_entries) {
|
||||
m_wrapped = true;
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// QUERIES
|
||||
//=============================================================================
|
||||
|
||||
std::vector<AuditEntry> AuditLog::GetEntries(size_t count) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
std::vector<AuditEntry> result;
|
||||
size_t stored = GetStoredEntries();
|
||||
count = std::min(count, stored);
|
||||
|
||||
result.reserve(count);
|
||||
|
||||
// Read from most recent backwards
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
size_t idx = (m_write_index + m_max_entries - 1 - i) % m_max_entries;
|
||||
result.push_back(m_entries[idx]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<AuditEntry> AuditLog::GetEntriesForApp(const std::string& app_id,
|
||||
size_t count) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
std::vector<AuditEntry> result;
|
||||
result.reserve(count);
|
||||
|
||||
size_t stored = GetStoredEntries();
|
||||
|
||||
for (size_t i = 0; i < stored && result.size() < count; i++) {
|
||||
size_t idx = (m_write_index + m_max_entries - 1 - i) % m_max_entries;
|
||||
if (m_entries[idx].app_id == app_id) {
|
||||
result.push_back(m_entries[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<AuditEntry> AuditLog::GetEntriesByEvent(AuditEvent event,
|
||||
size_t count) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
std::vector<AuditEntry> result;
|
||||
result.reserve(count);
|
||||
|
||||
size_t stored = GetStoredEntries();
|
||||
|
||||
for (size_t i = 0; i < stored && result.size() < count; i++) {
|
||||
size_t idx = (m_write_index + m_max_entries - 1 - i) % m_max_entries;
|
||||
if (m_entries[idx].event == event) {
|
||||
result.push_back(m_entries[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// STATISTICS
|
||||
//=============================================================================
|
||||
|
||||
size_t AuditLog::GetTotalEntries() const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_total_logged;
|
||||
}
|
||||
|
||||
size_t AuditLog::GetStoredEntries() const {
|
||||
// Note: caller should hold lock
|
||||
if (m_wrapped) {
|
||||
return m_max_entries;
|
||||
}
|
||||
return m_write_index;
|
||||
}
|
||||
|
||||
size_t AuditLog::CountEvents(AuditEvent event, const std::string& app_id) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
size_t count = 0;
|
||||
size_t stored = GetStoredEntries();
|
||||
|
||||
for (size_t i = 0; i < stored; i++) {
|
||||
const auto& entry = m_entries[i];
|
||||
if (entry.event == event) {
|
||||
if (app_id.empty() || entry.app_id == app_id) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// CLEAR
|
||||
//=============================================================================
|
||||
|
||||
void AuditLog::Clear() {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_write_index = 0;
|
||||
m_total_logged = 0;
|
||||
m_wrapped = false;
|
||||
// Clear all entries
|
||||
for (auto& entry : m_entries) {
|
||||
entry = AuditEntry{};
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// UTILITIES
|
||||
//=============================================================================
|
||||
|
||||
const char* AuditLog::EventToString(AuditEvent event) {
|
||||
switch (event) {
|
||||
case AuditEvent::AppStart: return "AppStart";
|
||||
case AuditEvent::AppStop: return "AppStop";
|
||||
case AuditEvent::PermissionCheck: return "PermissionCheck";
|
||||
case AuditEvent::PermissionGranted: return "PermissionGranted";
|
||||
case AuditEvent::PermissionDenied: return "PermissionDenied";
|
||||
case AuditEvent::NetworkRequest: return "NetworkRequest";
|
||||
case AuditEvent::NetworkBlocked: return "NetworkBlocked";
|
||||
case AuditEvent::FileAccess: return "FileAccess";
|
||||
case AuditEvent::FileBlocked: return "FileBlocked";
|
||||
case AuditEvent::DatabaseAccess: return "DatabaseAccess";
|
||||
case AuditEvent::CameraAccess: return "CameraAccess";
|
||||
case AuditEvent::MicrophoneAccess: return "MicrophoneAccess";
|
||||
case AuditEvent::LocationAccess: return "LocationAccess";
|
||||
case AuditEvent::SandboxViolation: return "SandboxViolation";
|
||||
case AuditEvent::ResourceLimitHit: return "ResourceLimitHit";
|
||||
case AuditEvent::RateLimitHit: return "RateLimitHit";
|
||||
case AuditEvent::Custom: return "Custom";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// GLOBAL INSTANCE
|
||||
//=============================================================================
|
||||
|
||||
AuditLog& GetAuditLog() {
|
||||
static AuditLog instance(10000);
|
||||
return instance;
|
||||
}
|
||||
|
||||
} // namespace mosis
|
||||
@@ -1,94 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
#include <chrono>
|
||||
|
||||
namespace mosis {
|
||||
|
||||
enum class AuditEvent {
|
||||
// Lifecycle
|
||||
AppStart,
|
||||
AppStop,
|
||||
|
||||
// Permissions
|
||||
PermissionCheck,
|
||||
PermissionGranted,
|
||||
PermissionDenied,
|
||||
|
||||
// Network
|
||||
NetworkRequest,
|
||||
NetworkBlocked,
|
||||
|
||||
// Storage
|
||||
FileAccess,
|
||||
FileBlocked,
|
||||
DatabaseAccess,
|
||||
|
||||
// Hardware
|
||||
CameraAccess,
|
||||
MicrophoneAccess,
|
||||
LocationAccess,
|
||||
|
||||
// Security
|
||||
SandboxViolation,
|
||||
ResourceLimitHit,
|
||||
RateLimitHit,
|
||||
|
||||
// Other
|
||||
Custom
|
||||
};
|
||||
|
||||
struct AuditEntry {
|
||||
std::chrono::system_clock::time_point timestamp;
|
||||
AuditEvent event;
|
||||
std::string app_id;
|
||||
std::string details;
|
||||
bool success;
|
||||
};
|
||||
|
||||
class AuditLog {
|
||||
public:
|
||||
explicit AuditLog(size_t max_entries = 10000);
|
||||
|
||||
// Log an event
|
||||
void Log(AuditEvent event, const std::string& app_id,
|
||||
const std::string& details = "", bool success = true);
|
||||
|
||||
// Query entries (returns most recent first)
|
||||
std::vector<AuditEntry> GetEntries(size_t count = 100) const;
|
||||
std::vector<AuditEntry> GetEntriesForApp(const std::string& app_id,
|
||||
size_t count = 100) const;
|
||||
std::vector<AuditEntry> GetEntriesByEvent(AuditEvent event,
|
||||
size_t count = 100) const;
|
||||
|
||||
// Statistics
|
||||
size_t GetTotalEntries() const;
|
||||
size_t GetStoredEntries() const;
|
||||
size_t CountEvents(AuditEvent event, const std::string& app_id = "") const;
|
||||
|
||||
// Clear all entries
|
||||
void Clear();
|
||||
|
||||
// Convert event to string for logging
|
||||
static const char* EventToString(AuditEvent event);
|
||||
|
||||
private:
|
||||
mutable std::mutex m_mutex;
|
||||
std::vector<AuditEntry> m_entries;
|
||||
size_t m_max_entries;
|
||||
size_t m_write_index = 0;
|
||||
size_t m_total_logged = 0;
|
||||
bool m_wrapped = false;
|
||||
};
|
||||
|
||||
// Global audit log (singleton)
|
||||
AuditLog& GetAuditLog();
|
||||
|
||||
} // namespace mosis
|
||||
|
||||
// Convenience alias
|
||||
using AuditLog = mosis::AuditLog;
|
||||
using AuditEvent = mosis::AuditEvent;
|
||||
using AuditEntry = mosis::AuditEntry;
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "camera_interface.h"
|
||||
#include "permission_gate.h"
|
||||
#include <mosis/sandbox/permission_gate.h>
|
||||
#include <lua.hpp>
|
||||
|
||||
namespace mosis {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Milestone 17: Contact access with granular read/write permissions
|
||||
|
||||
#include "contacts_interface.h"
|
||||
#include "lua_sandbox.h"
|
||||
#include <mosis/sandbox/lua_sandbox.h>
|
||||
#include <lua.hpp>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
@@ -1,393 +0,0 @@
|
||||
#include "crypto_api.h"
|
||||
|
||||
#include <lua.hpp>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <cstring>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <bcrypt.h>
|
||||
#pragma comment(lib, "bcrypt.lib")
|
||||
#endif
|
||||
|
||||
namespace mosis {
|
||||
|
||||
//=============================================================================
|
||||
// SECURE RANDOM
|
||||
//=============================================================================
|
||||
|
||||
SecureRandom::SecureRandom()
|
||||
: m_gen(m_rd()) {
|
||||
}
|
||||
|
||||
std::string SecureRandom::GetBytes(size_t count) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
std::string result(count, '\0');
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
result[i] = static_cast<char>(m_gen() & 0xFF);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int64_t SecureRandom::GetInt(int64_t min, int64_t max) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::uniform_int_distribution<int64_t> dist(min, max);
|
||||
return dist(m_gen);
|
||||
}
|
||||
|
||||
double SecureRandom::GetDouble() {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::uniform_real_distribution<double> dist(0.0, 1.0);
|
||||
return dist(m_gen);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// HASHING (Windows BCrypt)
|
||||
//=============================================================================
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
static std::string BytesToHex(const unsigned char* data, size_t len) {
|
||||
std::ostringstream oss;
|
||||
oss << std::hex << std::setfill('0');
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
oss << std::setw(2) << static_cast<int>(data[i]);
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
static LPCWSTR GetBCryptAlgorithm(HashAlgorithm algo) {
|
||||
switch (algo) {
|
||||
case HashAlgorithm::SHA256: return BCRYPT_SHA256_ALGORITHM;
|
||||
case HashAlgorithm::SHA512: return BCRYPT_SHA512_ALGORITHM;
|
||||
case HashAlgorithm::SHA1: return BCRYPT_SHA1_ALGORITHM;
|
||||
case HashAlgorithm::MD5: return BCRYPT_MD5_ALGORITHM;
|
||||
default: return BCRYPT_SHA256_ALGORITHM;
|
||||
}
|
||||
}
|
||||
|
||||
std::string ComputeHash(HashAlgorithm algo, const std::string& data) {
|
||||
BCRYPT_ALG_HANDLE hAlg = nullptr;
|
||||
BCRYPT_HASH_HANDLE hHash = nullptr;
|
||||
NTSTATUS status;
|
||||
std::string result;
|
||||
|
||||
status = BCryptOpenAlgorithmProvider(&hAlg, GetBCryptAlgorithm(algo), nullptr, 0);
|
||||
if (!BCRYPT_SUCCESS(status)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
DWORD hashLength = 0;
|
||||
DWORD resultLength = 0;
|
||||
status = BCryptGetProperty(hAlg, BCRYPT_HASH_LENGTH, (PUCHAR)&hashLength,
|
||||
sizeof(hashLength), &resultLength, 0);
|
||||
if (!BCRYPT_SUCCESS(status)) {
|
||||
BCryptCloseAlgorithmProvider(hAlg, 0);
|
||||
return "";
|
||||
}
|
||||
|
||||
std::vector<unsigned char> hashBuffer(hashLength);
|
||||
|
||||
status = BCryptCreateHash(hAlg, &hHash, nullptr, 0, nullptr, 0, 0);
|
||||
if (!BCRYPT_SUCCESS(status)) {
|
||||
BCryptCloseAlgorithmProvider(hAlg, 0);
|
||||
return "";
|
||||
}
|
||||
|
||||
status = BCryptHashData(hHash, (PUCHAR)data.data(), static_cast<ULONG>(data.size()), 0);
|
||||
if (!BCRYPT_SUCCESS(status)) {
|
||||
BCryptDestroyHash(hHash);
|
||||
BCryptCloseAlgorithmProvider(hAlg, 0);
|
||||
return "";
|
||||
}
|
||||
|
||||
status = BCryptFinishHash(hHash, hashBuffer.data(), hashLength, 0);
|
||||
if (BCRYPT_SUCCESS(status)) {
|
||||
result = BytesToHex(hashBuffer.data(), hashLength);
|
||||
}
|
||||
|
||||
BCryptDestroyHash(hHash);
|
||||
BCryptCloseAlgorithmProvider(hAlg, 0);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string ComputeHMAC(HashAlgorithm algo, const std::string& key, const std::string& data) {
|
||||
BCRYPT_ALG_HANDLE hAlg = nullptr;
|
||||
BCRYPT_HASH_HANDLE hHash = nullptr;
|
||||
NTSTATUS status;
|
||||
std::string result;
|
||||
|
||||
status = BCryptOpenAlgorithmProvider(&hAlg, GetBCryptAlgorithm(algo), nullptr,
|
||||
BCRYPT_ALG_HANDLE_HMAC_FLAG);
|
||||
if (!BCRYPT_SUCCESS(status)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
DWORD hashLength = 0;
|
||||
DWORD resultLength = 0;
|
||||
status = BCryptGetProperty(hAlg, BCRYPT_HASH_LENGTH, (PUCHAR)&hashLength,
|
||||
sizeof(hashLength), &resultLength, 0);
|
||||
if (!BCRYPT_SUCCESS(status)) {
|
||||
BCryptCloseAlgorithmProvider(hAlg, 0);
|
||||
return "";
|
||||
}
|
||||
|
||||
std::vector<unsigned char> hashBuffer(hashLength);
|
||||
|
||||
status = BCryptCreateHash(hAlg, &hHash, nullptr, 0,
|
||||
(PUCHAR)key.data(), static_cast<ULONG>(key.size()), 0);
|
||||
if (!BCRYPT_SUCCESS(status)) {
|
||||
BCryptCloseAlgorithmProvider(hAlg, 0);
|
||||
return "";
|
||||
}
|
||||
|
||||
status = BCryptHashData(hHash, (PUCHAR)data.data(), static_cast<ULONG>(data.size()), 0);
|
||||
if (!BCRYPT_SUCCESS(status)) {
|
||||
BCryptDestroyHash(hHash);
|
||||
BCryptCloseAlgorithmProvider(hAlg, 0);
|
||||
return "";
|
||||
}
|
||||
|
||||
status = BCryptFinishHash(hHash, hashBuffer.data(), hashLength, 0);
|
||||
if (BCRYPT_SUCCESS(status)) {
|
||||
result = BytesToHex(hashBuffer.data(), hashLength);
|
||||
}
|
||||
|
||||
BCryptDestroyHash(hHash);
|
||||
BCryptCloseAlgorithmProvider(hAlg, 0);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
// Stub implementations for non-Windows (would need OpenSSL or similar)
|
||||
std::string ComputeHash(HashAlgorithm algo, const std::string& data) {
|
||||
(void)algo;
|
||||
(void)data;
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string ComputeHMAC(HashAlgorithm algo, const std::string& key, const std::string& data) {
|
||||
(void)algo;
|
||||
(void)key;
|
||||
(void)data;
|
||||
return "";
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
//=============================================================================
|
||||
// LUA CRYPTO API
|
||||
//=============================================================================
|
||||
|
||||
static const char* CRYPTO_RNG_KEY = "__mosis_crypto_rng";
|
||||
|
||||
static SecureRandom* GetRng(lua_State* L) {
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, CRYPTO_RNG_KEY);
|
||||
if (lua_islightuserdata(L, -1)) {
|
||||
SecureRandom* rng = static_cast<SecureRandom*>(lua_touserdata(L, -1));
|
||||
lua_pop(L, 1);
|
||||
return rng;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Create a default RNG if none registered
|
||||
static SecureRandom default_rng;
|
||||
return &default_rng;
|
||||
}
|
||||
|
||||
// crypto.randomBytes(n) -> string
|
||||
static int lua_crypto_randomBytes(lua_State* L) {
|
||||
lua_Integer n = luaL_checkinteger(L, 1);
|
||||
|
||||
if (n < 0) {
|
||||
return luaL_error(L, "crypto.randomBytes: count must be non-negative");
|
||||
}
|
||||
if (n > 1024) {
|
||||
return luaL_error(L, "crypto.randomBytes: count must not exceed 1024");
|
||||
}
|
||||
|
||||
SecureRandom* rng = GetRng(L);
|
||||
std::string bytes = rng->GetBytes(static_cast<size_t>(n));
|
||||
|
||||
lua_pushlstring(L, bytes.data(), bytes.size());
|
||||
return 1;
|
||||
}
|
||||
|
||||
static HashAlgorithm ParseAlgorithm(const char* name) {
|
||||
if (strcmp(name, "sha256") == 0) return HashAlgorithm::SHA256;
|
||||
if (strcmp(name, "sha512") == 0) return HashAlgorithm::SHA512;
|
||||
if (strcmp(name, "sha1") == 0) return HashAlgorithm::SHA1;
|
||||
if (strcmp(name, "md5") == 0) return HashAlgorithm::MD5;
|
||||
return HashAlgorithm::SHA256; // Default
|
||||
}
|
||||
|
||||
// crypto.hash(algorithm, data) -> string
|
||||
static int lua_crypto_hash(lua_State* L) {
|
||||
const char* algo_name = luaL_checkstring(L, 1);
|
||||
size_t data_len;
|
||||
const char* data = luaL_checklstring(L, 2, &data_len);
|
||||
|
||||
// Limit input size
|
||||
if (data_len > 10 * 1024 * 1024) {
|
||||
return luaL_error(L, "crypto.hash: input too large (max 10MB)");
|
||||
}
|
||||
|
||||
HashAlgorithm algo = ParseAlgorithm(algo_name);
|
||||
std::string result = ComputeHash(algo, std::string(data, data_len));
|
||||
|
||||
if (result.empty()) {
|
||||
return luaL_error(L, "crypto.hash: failed to compute hash");
|
||||
}
|
||||
|
||||
lua_pushstring(L, result.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// crypto.hmac(algorithm, key, data) -> string
|
||||
static int lua_crypto_hmac(lua_State* L) {
|
||||
const char* algo_name = luaL_checkstring(L, 1);
|
||||
size_t key_len;
|
||||
const char* key = luaL_checklstring(L, 2, &key_len);
|
||||
size_t data_len;
|
||||
const char* data = luaL_checklstring(L, 3, &data_len);
|
||||
|
||||
// Limit input sizes
|
||||
if (key_len > 1024) {
|
||||
return luaL_error(L, "crypto.hmac: key too large (max 1KB)");
|
||||
}
|
||||
if (data_len > 10 * 1024 * 1024) {
|
||||
return luaL_error(L, "crypto.hmac: data too large (max 10MB)");
|
||||
}
|
||||
|
||||
HashAlgorithm algo = ParseAlgorithm(algo_name);
|
||||
std::string result = ComputeHMAC(algo, std::string(key, key_len),
|
||||
std::string(data, data_len));
|
||||
|
||||
if (result.empty()) {
|
||||
return luaL_error(L, "crypto.hmac: failed to compute HMAC");
|
||||
}
|
||||
|
||||
lua_pushstring(L, result.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Helper to set a global in the real _G (bypassing any proxy)
|
||||
static void SetGlobalInRealG(lua_State* L, const char* name) {
|
||||
// Stack: value to set as global
|
||||
|
||||
// Get _G (might be a proxy)
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
|
||||
|
||||
// Check if it has a metatable with __index (proxy pattern)
|
||||
if (lua_getmetatable(L, -1)) {
|
||||
lua_getfield(L, -1, "__index");
|
||||
if (lua_istable(L, -1)) {
|
||||
// Found real _G through proxy's __index
|
||||
// Stack: value, proxy, mt, real_G
|
||||
lua_pushvalue(L, -4); // Copy value
|
||||
lua_setfield(L, -2, name); // real_G[name] = value
|
||||
lua_pop(L, 4); // pop real_G, mt, proxy, original value
|
||||
return;
|
||||
}
|
||||
lua_pop(L, 2); // pop __index, metatable
|
||||
}
|
||||
|
||||
// No proxy, set directly in _G
|
||||
// Stack: value, _G
|
||||
lua_pushvalue(L, -2); // Copy value
|
||||
lua_setfield(L, -2, name); // _G[name] = value
|
||||
lua_pop(L, 2); // pop _G, original value
|
||||
}
|
||||
|
||||
void RegisterCryptoAPI(lua_State* L) {
|
||||
// Create crypto table
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushcfunction(L, lua_crypto_randomBytes);
|
||||
lua_setfield(L, -2, "randomBytes");
|
||||
|
||||
lua_pushcfunction(L, lua_crypto_hash);
|
||||
lua_setfield(L, -2, "hash");
|
||||
|
||||
lua_pushcfunction(L, lua_crypto_hmac);
|
||||
lua_setfield(L, -2, "hmac");
|
||||
|
||||
// Set as global (bypassing proxy)
|
||||
SetGlobalInRealG(L, "crypto");
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// SECURE MATH.RANDOM
|
||||
//=============================================================================
|
||||
|
||||
static const char* MATH_RNG_KEY = "__mosis_math_rng";
|
||||
|
||||
// math.random([m [, n]]) - secure version
|
||||
static int lua_secure_random(lua_State* L) {
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, MATH_RNG_KEY);
|
||||
if (!lua_islightuserdata(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
return luaL_error(L, "math.random: RNG not initialized");
|
||||
}
|
||||
SecureRandom* rng = static_cast<SecureRandom*>(lua_touserdata(L, -1));
|
||||
lua_pop(L, 1);
|
||||
|
||||
int nargs = lua_gettop(L);
|
||||
|
||||
if (nargs == 0) {
|
||||
// Return double in [0.0, 1.0)
|
||||
lua_pushnumber(L, rng->GetDouble());
|
||||
return 1;
|
||||
} else if (nargs == 1) {
|
||||
// Return integer in [1, n]
|
||||
lua_Integer n = luaL_checkinteger(L, 1);
|
||||
if (n < 1) {
|
||||
return luaL_error(L, "math.random: interval is empty");
|
||||
}
|
||||
lua_pushinteger(L, rng->GetInt(1, n));
|
||||
return 1;
|
||||
} else {
|
||||
// Return integer in [m, n]
|
||||
lua_Integer m = luaL_checkinteger(L, 1);
|
||||
lua_Integer n = luaL_checkinteger(L, 2);
|
||||
if (m > n) {
|
||||
return luaL_error(L, "math.random: interval is empty");
|
||||
}
|
||||
lua_pushinteger(L, rng->GetInt(m, n));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
void RegisterSecureMathRandom(lua_State* L, SecureRandom* rng) {
|
||||
// Store RNG in registry
|
||||
lua_pushlightuserdata(L, rng);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, MATH_RNG_KEY);
|
||||
|
||||
// Also store for crypto API
|
||||
lua_pushlightuserdata(L, rng);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, CRYPTO_RNG_KEY);
|
||||
|
||||
// Get the math table
|
||||
lua_getglobal(L, "math");
|
||||
if (!lua_istable(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace math.random with secure version
|
||||
lua_pushcfunction(L, lua_secure_random);
|
||||
lua_setfield(L, -2, "random");
|
||||
|
||||
// Remove math.randomseed
|
||||
lua_pushnil(L);
|
||||
lua_setfield(L, -2, "randomseed");
|
||||
|
||||
lua_pop(L, 1); // Pop math table
|
||||
}
|
||||
|
||||
} // namespace mosis
|
||||
@@ -1,52 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <random>
|
||||
#include <mutex>
|
||||
|
||||
struct lua_State;
|
||||
|
||||
namespace mosis {
|
||||
|
||||
// Per-app cryptographically secure RNG
|
||||
class SecureRandom {
|
||||
public:
|
||||
SecureRandom();
|
||||
|
||||
// Get random bytes as binary string
|
||||
std::string GetBytes(size_t count);
|
||||
|
||||
// Get random integer in range [min, max]
|
||||
int64_t GetInt(int64_t min, int64_t max);
|
||||
|
||||
// Get random double in range [0.0, 1.0)
|
||||
double GetDouble();
|
||||
|
||||
private:
|
||||
std::random_device m_rd;
|
||||
std::mt19937_64 m_gen;
|
||||
std::mutex m_mutex;
|
||||
};
|
||||
|
||||
// Hash algorithms supported
|
||||
enum class HashAlgorithm {
|
||||
SHA256,
|
||||
SHA512,
|
||||
SHA1,
|
||||
MD5
|
||||
};
|
||||
|
||||
// Compute hash of data
|
||||
std::string ComputeHash(HashAlgorithm algo, const std::string& data);
|
||||
|
||||
// Compute HMAC of data with key
|
||||
std::string ComputeHMAC(HashAlgorithm algo, const std::string& key, const std::string& data);
|
||||
|
||||
// Register crypto.* APIs as globals
|
||||
void RegisterCryptoAPI(lua_State* L);
|
||||
|
||||
// Register secure math.random replacement (removes math.randomseed)
|
||||
void RegisterSecureMathRandom(lua_State* L, SecureRandom* rng);
|
||||
|
||||
} // namespace mosis
|
||||
@@ -1,598 +0,0 @@
|
||||
#include "database_manager.h"
|
||||
#include <sqlite3.h>
|
||||
#include <lua.hpp>
|
||||
#include <filesystem>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace mosis {
|
||||
|
||||
// Helper to set a global in the real _G (bypassing any proxy)
|
||||
static void SetGlobalInRealG(lua_State* L, const char* name) {
|
||||
// Stack: value to set as global
|
||||
|
||||
// Get _G (might be a proxy)
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
|
||||
|
||||
// Check if it has a metatable with __index (proxy pattern)
|
||||
if (lua_getmetatable(L, -1)) {
|
||||
lua_getfield(L, -1, "__index");
|
||||
if (lua_istable(L, -1)) {
|
||||
// Found real _G through proxy's __index
|
||||
// Stack: value, proxy, mt, real_G
|
||||
lua_pushvalue(L, -4); // Copy value
|
||||
lua_setfield(L, -2, name); // real_G[name] = value
|
||||
lua_pop(L, 4); // pop real_G, mt, proxy, original value
|
||||
return;
|
||||
}
|
||||
lua_pop(L, 2); // pop __index, metatable
|
||||
}
|
||||
|
||||
// No proxy, set directly in _G
|
||||
// Stack: value, _G
|
||||
lua_pushvalue(L, -2); // Copy value
|
||||
lua_setfield(L, -2, name); // _G[name] = value
|
||||
lua_pop(L, 2); // pop _G, original value
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DatabaseManager
|
||||
// ============================================================================
|
||||
|
||||
DatabaseManager::DatabaseManager(const std::string& app_id,
|
||||
const std::string& app_root,
|
||||
const DatabaseLimits& limits)
|
||||
: m_app_id(app_id)
|
||||
, m_app_root(app_root)
|
||||
, m_limits(limits) {
|
||||
}
|
||||
|
||||
DatabaseManager::~DatabaseManager() {
|
||||
CloseAll();
|
||||
}
|
||||
|
||||
bool DatabaseManager::ValidateName(const std::string& name, std::string& error) {
|
||||
if (name.empty()) {
|
||||
error = "Database name cannot be empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name.length() > 64) {
|
||||
error = "Database name too long (max 64 characters)";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for path traversal
|
||||
if (name.find("..") != std::string::npos) {
|
||||
error = "Database name contains invalid path traversal";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for path separators
|
||||
if (name.find('/') != std::string::npos || name.find('\\') != std::string::npos) {
|
||||
error = "Database name cannot contain path separators";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only allow alphanumeric, underscore, hyphen
|
||||
for (char c : name) {
|
||||
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '_' && c != '-') {
|
||||
error = "Database name contains invalid characters (only alphanumeric, underscore, hyphen allowed)";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string DatabaseManager::ResolvePath(const std::string& name) {
|
||||
fs::path db_dir = fs::path(m_app_root) / "db";
|
||||
return (db_dir / (name + ".db")).string();
|
||||
}
|
||||
|
||||
std::shared_ptr<DatabaseHandle> DatabaseManager::Open(const std::string& name, std::string& error) {
|
||||
// Validate name
|
||||
if (!ValidateName(name, error)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check if already open
|
||||
auto it = m_databases.find(name);
|
||||
if (it != m_databases.end() && it->second->IsOpen()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Check max databases limit
|
||||
if (m_databases.size() >= static_cast<size_t>(m_limits.max_databases_per_app)) {
|
||||
error = "Maximum number of open databases reached";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Resolve path and ensure directory exists
|
||||
std::string db_path = ResolvePath(name);
|
||||
fs::path parent = fs::path(db_path).parent_path();
|
||||
|
||||
std::error_code ec;
|
||||
fs::create_directories(parent, ec);
|
||||
if (ec) {
|
||||
error = "Failed to create database directory: " + ec.message();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Open SQLite database
|
||||
sqlite3* db = nullptr;
|
||||
int rc = sqlite3_open(db_path.c_str(), &db);
|
||||
if (rc != SQLITE_OK) {
|
||||
error = "Failed to open database: " + std::string(sqlite3_errmsg(db));
|
||||
sqlite3_close(db);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Create handle
|
||||
auto handle = std::make_shared<DatabaseHandle>(db, db_path, m_limits);
|
||||
m_databases[name] = handle;
|
||||
return handle;
|
||||
}
|
||||
|
||||
void DatabaseManager::CloseAll() {
|
||||
for (auto& [name, handle] : m_databases) {
|
||||
if (handle) {
|
||||
handle->Close();
|
||||
}
|
||||
}
|
||||
m_databases.clear();
|
||||
}
|
||||
|
||||
size_t DatabaseManager::GetOpenDatabaseCount() const {
|
||||
size_t count = 0;
|
||||
for (const auto& [name, handle] : m_databases) {
|
||||
if (handle && handle->IsOpen()) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DatabaseHandle
|
||||
// ============================================================================
|
||||
|
||||
DatabaseHandle::DatabaseHandle(sqlite3* db, const std::string& path, const DatabaseLimits& limits)
|
||||
: m_db(db)
|
||||
, m_path(path)
|
||||
, m_limits(limits) {
|
||||
|
||||
if (m_db) {
|
||||
// Set up authorizer
|
||||
sqlite3_set_authorizer(m_db, Authorizer, this);
|
||||
|
||||
// Set busy timeout
|
||||
sqlite3_busy_timeout(m_db, m_limits.max_query_time_ms);
|
||||
}
|
||||
}
|
||||
|
||||
DatabaseHandle::~DatabaseHandle() {
|
||||
Close();
|
||||
}
|
||||
|
||||
void DatabaseHandle::Close() {
|
||||
if (m_db) {
|
||||
sqlite3_close(m_db);
|
||||
m_db = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int DatabaseHandle::Authorizer(void* user_data, int action, const char* arg1,
|
||||
const char* arg2, const char* arg3, const char* arg4) {
|
||||
(void)user_data;
|
||||
(void)arg3;
|
||||
(void)arg4;
|
||||
|
||||
switch (action) {
|
||||
case SQLITE_ATTACH:
|
||||
case SQLITE_DETACH:
|
||||
// Block attaching/detaching databases
|
||||
return SQLITE_DENY;
|
||||
|
||||
case SQLITE_PRAGMA: {
|
||||
// Allow safe pragmas only
|
||||
if (arg1) {
|
||||
std::string pragma(arg1);
|
||||
// Convert to lowercase for comparison
|
||||
std::transform(pragma.begin(), pragma.end(), pragma.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
|
||||
// Whitelist of safe pragmas
|
||||
if (pragma == "table_info" ||
|
||||
pragma == "index_list" ||
|
||||
pragma == "index_info" ||
|
||||
pragma == "foreign_keys" ||
|
||||
pragma == "foreign_key_list" ||
|
||||
pragma == "database_list" ||
|
||||
pragma == "table_list" ||
|
||||
pragma == "integrity_check" ||
|
||||
pragma == "quick_check") {
|
||||
return SQLITE_OK;
|
||||
}
|
||||
// Block all other pragmas
|
||||
return SQLITE_DENY;
|
||||
}
|
||||
return SQLITE_DENY;
|
||||
}
|
||||
|
||||
case SQLITE_FUNCTION: {
|
||||
// Block dangerous functions
|
||||
if (arg2) {
|
||||
std::string func(arg2);
|
||||
std::transform(func.begin(), func.end(), func.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
if (func == "load_extension") {
|
||||
return SQLITE_DENY;
|
||||
}
|
||||
}
|
||||
return SQLITE_OK;
|
||||
}
|
||||
|
||||
default:
|
||||
return SQLITE_OK;
|
||||
}
|
||||
}
|
||||
|
||||
bool DatabaseHandle::BindParameters(void* stmt_ptr, const std::vector<SqlValue>& params, std::string& error) {
|
||||
sqlite3_stmt* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
|
||||
|
||||
for (size_t i = 0; i < params.size(); i++) {
|
||||
int idx = static_cast<int>(i + 1); // SQLite parameters are 1-indexed
|
||||
int rc = SQLITE_OK;
|
||||
|
||||
std::visit([&](auto&& arg) {
|
||||
using T = std::decay_t<decltype(arg)>;
|
||||
if constexpr (std::is_same_v<T, std::nullptr_t>) {
|
||||
rc = sqlite3_bind_null(stmt, idx);
|
||||
} else if constexpr (std::is_same_v<T, int64_t>) {
|
||||
rc = sqlite3_bind_int64(stmt, idx, arg);
|
||||
} else if constexpr (std::is_same_v<T, double>) {
|
||||
rc = sqlite3_bind_double(stmt, idx, arg);
|
||||
} else if constexpr (std::is_same_v<T, std::string>) {
|
||||
rc = sqlite3_bind_text(stmt, idx, arg.c_str(), static_cast<int>(arg.size()), SQLITE_TRANSIENT);
|
||||
} else if constexpr (std::is_same_v<T, std::vector<uint8_t>>) {
|
||||
rc = sqlite3_bind_blob(stmt, idx, arg.data(), static_cast<int>(arg.size()), SQLITE_TRANSIENT);
|
||||
}
|
||||
}, params[i]);
|
||||
|
||||
if (rc != SQLITE_OK) {
|
||||
error = "Failed to bind parameter " + std::to_string(i) + ": " + sqlite3_errmsg(m_db);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DatabaseHandle::Execute(const std::string& sql, const std::vector<SqlValue>& params, std::string& error) {
|
||||
if (!m_db) {
|
||||
error = "Database not open";
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
int rc = sqlite3_prepare_v2(m_db, sql.c_str(), static_cast<int>(sql.size()), &stmt, nullptr);
|
||||
if (rc != SQLITE_OK) {
|
||||
error = "SQL prepare error: " + std::string(sqlite3_errmsg(m_db));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!BindParameters(stmt, params, error)) {
|
||||
sqlite3_finalize(stmt);
|
||||
return false;
|
||||
}
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (rc != SQLITE_DONE && rc != SQLITE_ROW) {
|
||||
error = "SQL execution error: " + std::string(sqlite3_errmsg(m_db));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<SqlResult> DatabaseHandle::Query(const std::string& sql, const std::vector<SqlValue>& params,
|
||||
std::string& error) {
|
||||
if (!m_db) {
|
||||
error = "Database not open";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
int rc = sqlite3_prepare_v2(m_db, sql.c_str(), static_cast<int>(sql.size()), &stmt, nullptr);
|
||||
if (rc != SQLITE_OK) {
|
||||
error = "SQL prepare error: " + std::string(sqlite3_errmsg(m_db));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!BindParameters(stmt, params, error)) {
|
||||
sqlite3_finalize(stmt);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
SqlResult result;
|
||||
int row_count = 0;
|
||||
|
||||
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
|
||||
if (row_count >= m_limits.max_result_rows) {
|
||||
error = "Result row limit exceeded";
|
||||
sqlite3_finalize(stmt);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
int col_count = sqlite3_column_count(stmt);
|
||||
SqlRow row;
|
||||
row.reserve(col_count);
|
||||
|
||||
for (int i = 0; i < col_count; i++) {
|
||||
int type = sqlite3_column_type(stmt, i);
|
||||
switch (type) {
|
||||
case SQLITE_NULL:
|
||||
row.push_back(nullptr);
|
||||
break;
|
||||
case SQLITE_INTEGER:
|
||||
row.push_back(sqlite3_column_int64(stmt, i));
|
||||
break;
|
||||
case SQLITE_FLOAT:
|
||||
row.push_back(sqlite3_column_double(stmt, i));
|
||||
break;
|
||||
case SQLITE_TEXT: {
|
||||
const char* text = reinterpret_cast<const char*>(sqlite3_column_text(stmt, i));
|
||||
int len = sqlite3_column_bytes(stmt, i);
|
||||
row.push_back(std::string(text, len));
|
||||
break;
|
||||
}
|
||||
case SQLITE_BLOB: {
|
||||
const uint8_t* data = static_cast<const uint8_t*>(sqlite3_column_blob(stmt, i));
|
||||
int len = sqlite3_column_bytes(stmt, i);
|
||||
row.push_back(std::vector<uint8_t>(data, data + len));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.push_back(std::move(row));
|
||||
row_count++;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (rc != SQLITE_DONE) {
|
||||
error = "SQL query error: " + std::string(sqlite3_errmsg(m_db));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int64_t DatabaseHandle::GetLastInsertRowId() const {
|
||||
if (!m_db) return 0;
|
||||
return sqlite3_last_insert_rowid(m_db);
|
||||
}
|
||||
|
||||
int DatabaseHandle::GetChanges() const {
|
||||
if (!m_db) return 0;
|
||||
return sqlite3_changes(m_db);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Lua API
|
||||
// ============================================================================
|
||||
|
||||
struct LuaDatabaseHandle {
|
||||
std::shared_ptr<DatabaseHandle> handle;
|
||||
};
|
||||
|
||||
static int Lua_DatabaseHandle_Execute(lua_State* L) {
|
||||
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(luaL_checkudata(L, 1, "DatabaseHandle"));
|
||||
if (!lh->handle || !lh->handle->IsOpen()) {
|
||||
lua_pushboolean(L, 0);
|
||||
lua_pushstring(L, "Database not open");
|
||||
return 2;
|
||||
}
|
||||
|
||||
const char* sql = luaL_checkstring(L, 2);
|
||||
|
||||
// Get parameters from optional table
|
||||
std::vector<SqlValue> params;
|
||||
if (lua_gettop(L) >= 3 && lua_istable(L, 3)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, 3) != 0) {
|
||||
if (lua_isnil(L, -1)) {
|
||||
params.push_back(nullptr);
|
||||
} else if (lua_isinteger(L, -1)) {
|
||||
params.push_back(static_cast<int64_t>(lua_tointeger(L, -1)));
|
||||
} else if (lua_isnumber(L, -1)) {
|
||||
params.push_back(lua_tonumber(L, -1));
|
||||
} else if (lua_isstring(L, -1)) {
|
||||
size_t len;
|
||||
const char* str = lua_tolstring(L, -1, &len);
|
||||
params.push_back(std::string(str, len));
|
||||
} else if (lua_isboolean(L, -1)) {
|
||||
params.push_back(static_cast<int64_t>(lua_toboolean(L, -1)));
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
|
||||
std::string error;
|
||||
if (lh->handle->Execute(sql, params, error)) {
|
||||
lua_pushboolean(L, 1);
|
||||
return 1;
|
||||
} else {
|
||||
lua_pushboolean(L, 0);
|
||||
lua_pushstring(L, error.c_str());
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
static int Lua_DatabaseHandle_Query(lua_State* L) {
|
||||
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(luaL_checkudata(L, 1, "DatabaseHandle"));
|
||||
if (!lh->handle || !lh->handle->IsOpen()) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, "Database not open");
|
||||
return 2;
|
||||
}
|
||||
|
||||
const char* sql = luaL_checkstring(L, 2);
|
||||
|
||||
// Get parameters from optional table
|
||||
std::vector<SqlValue> params;
|
||||
if (lua_gettop(L) >= 3 && lua_istable(L, 3)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, 3) != 0) {
|
||||
if (lua_isnil(L, -1)) {
|
||||
params.push_back(nullptr);
|
||||
} else if (lua_isinteger(L, -1)) {
|
||||
params.push_back(static_cast<int64_t>(lua_tointeger(L, -1)));
|
||||
} else if (lua_isnumber(L, -1)) {
|
||||
params.push_back(lua_tonumber(L, -1));
|
||||
} else if (lua_isstring(L, -1)) {
|
||||
size_t len;
|
||||
const char* str = lua_tolstring(L, -1, &len);
|
||||
params.push_back(std::string(str, len));
|
||||
} else if (lua_isboolean(L, -1)) {
|
||||
params.push_back(static_cast<int64_t>(lua_toboolean(L, -1)));
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
|
||||
std::string error;
|
||||
auto result = lh->handle->Query(sql, params, error);
|
||||
|
||||
if (!result.has_value()) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, error.c_str());
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Create result table
|
||||
lua_createtable(L, static_cast<int>(result->size()), 0);
|
||||
int row_idx = 1;
|
||||
for (const auto& row : *result) {
|
||||
lua_createtable(L, static_cast<int>(row.size()), 0);
|
||||
int col_idx = 1;
|
||||
for (const auto& val : row) {
|
||||
std::visit([L](auto&& arg) {
|
||||
using T = std::decay_t<decltype(arg)>;
|
||||
if constexpr (std::is_same_v<T, std::nullptr_t>) {
|
||||
lua_pushnil(L);
|
||||
} else if constexpr (std::is_same_v<T, int64_t>) {
|
||||
lua_pushinteger(L, arg);
|
||||
} else if constexpr (std::is_same_v<T, double>) {
|
||||
lua_pushnumber(L, arg);
|
||||
} else if constexpr (std::is_same_v<T, std::string>) {
|
||||
lua_pushlstring(L, arg.c_str(), arg.size());
|
||||
} else if constexpr (std::is_same_v<T, std::vector<uint8_t>>) {
|
||||
lua_pushlstring(L, reinterpret_cast<const char*>(arg.data()), arg.size());
|
||||
}
|
||||
}, val);
|
||||
lua_rawseti(L, -2, col_idx++);
|
||||
}
|
||||
lua_rawseti(L, -2, row_idx++);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int Lua_DatabaseHandle_LastInsertId(lua_State* L) {
|
||||
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(luaL_checkudata(L, 1, "DatabaseHandle"));
|
||||
if (!lh->handle) {
|
||||
lua_pushinteger(L, 0);
|
||||
return 1;
|
||||
}
|
||||
lua_pushinteger(L, lh->handle->GetLastInsertRowId());
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int Lua_DatabaseHandle_Changes(lua_State* L) {
|
||||
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(luaL_checkudata(L, 1, "DatabaseHandle"));
|
||||
if (!lh->handle) {
|
||||
lua_pushinteger(L, 0);
|
||||
return 1;
|
||||
}
|
||||
lua_pushinteger(L, lh->handle->GetChanges());
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int Lua_DatabaseHandle_Close(lua_State* L) {
|
||||
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(luaL_checkudata(L, 1, "DatabaseHandle"));
|
||||
if (lh->handle) {
|
||||
lh->handle->Close();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int Lua_DatabaseHandle_GC(lua_State* L) {
|
||||
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(luaL_checkudata(L, 1, "DatabaseHandle"));
|
||||
lh->~LuaDatabaseHandle();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const luaL_Reg DatabaseHandle_methods[] = {
|
||||
{"execute", Lua_DatabaseHandle_Execute},
|
||||
{"query", Lua_DatabaseHandle_Query},
|
||||
{"lastInsertId", Lua_DatabaseHandle_LastInsertId},
|
||||
{"changes", Lua_DatabaseHandle_Changes},
|
||||
{"close", Lua_DatabaseHandle_Close},
|
||||
{nullptr, nullptr}
|
||||
};
|
||||
|
||||
static int Lua_Database_Open(lua_State* L) {
|
||||
DatabaseManager* manager = static_cast<DatabaseManager*>(lua_touserdata(L, lua_upvalueindex(1)));
|
||||
const char* name = luaL_checkstring(L, 1);
|
||||
|
||||
std::string error;
|
||||
auto handle = manager->Open(name, error);
|
||||
|
||||
if (!handle) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, error.c_str());
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Create userdata
|
||||
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(lua_newuserdata(L, sizeof(LuaDatabaseHandle)));
|
||||
new (lh) LuaDatabaseHandle{handle};
|
||||
|
||||
luaL_getmetatable(L, "DatabaseHandle");
|
||||
lua_setmetatable(L, -2);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterDatabaseAPI(lua_State* L, DatabaseManager* manager) {
|
||||
// Create DatabaseHandle metatable
|
||||
luaL_newmetatable(L, "DatabaseHandle");
|
||||
|
||||
lua_pushvalue(L, -1);
|
||||
lua_setfield(L, -2, "__index");
|
||||
|
||||
lua_pushcfunction(L, Lua_DatabaseHandle_GC);
|
||||
lua_setfield(L, -2, "__gc");
|
||||
|
||||
luaL_setfuncs(L, DatabaseHandle_methods, 0);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Create database table
|
||||
lua_newtable(L);
|
||||
|
||||
// database.open
|
||||
lua_pushlightuserdata(L, manager);
|
||||
lua_pushcclosure(L, Lua_Database_Open, 1);
|
||||
lua_setfield(L, -2, "open");
|
||||
|
||||
// Set as global
|
||||
SetGlobalInRealG(L, "database");
|
||||
}
|
||||
|
||||
} // namespace mosis
|
||||
@@ -1,88 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <variant>
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
|
||||
struct sqlite3;
|
||||
struct lua_State;
|
||||
|
||||
namespace mosis {
|
||||
|
||||
// SQL value types
|
||||
using SqlValue = std::variant<std::nullptr_t, int64_t, double, std::string, std::vector<uint8_t>>;
|
||||
using SqlRow = std::vector<SqlValue>;
|
||||
using SqlResult = std::vector<SqlRow>;
|
||||
|
||||
struct DatabaseLimits {
|
||||
size_t max_database_size = 50 * 1024 * 1024; // 50 MB per database
|
||||
int max_databases_per_app = 5; // Max open databases
|
||||
int max_query_time_ms = 5000; // 5 second query timeout
|
||||
int max_result_rows = 10000; // Max rows returned
|
||||
};
|
||||
|
||||
class DatabaseHandle;
|
||||
|
||||
class DatabaseManager {
|
||||
public:
|
||||
DatabaseManager(const std::string& app_id,
|
||||
const std::string& app_root,
|
||||
const DatabaseLimits& limits = DatabaseLimits{});
|
||||
~DatabaseManager();
|
||||
|
||||
// Database operations
|
||||
std::shared_ptr<DatabaseHandle> Open(const std::string& name, std::string& error);
|
||||
void CloseAll();
|
||||
|
||||
// Stats
|
||||
size_t GetOpenDatabaseCount() const;
|
||||
|
||||
private:
|
||||
std::string m_app_id;
|
||||
std::string m_app_root;
|
||||
DatabaseLimits m_limits;
|
||||
std::unordered_map<std::string, std::shared_ptr<DatabaseHandle>> m_databases;
|
||||
|
||||
std::string ResolvePath(const std::string& name);
|
||||
bool ValidateName(const std::string& name, std::string& error);
|
||||
};
|
||||
|
||||
class DatabaseHandle {
|
||||
public:
|
||||
DatabaseHandle(sqlite3* db, const std::string& path, const DatabaseLimits& limits);
|
||||
~DatabaseHandle();
|
||||
|
||||
// Execute (INSERT, UPDATE, DELETE, CREATE, etc.)
|
||||
bool Execute(const std::string& sql, const std::vector<SqlValue>& params, std::string& error);
|
||||
|
||||
// Query (SELECT)
|
||||
std::optional<SqlResult> Query(const std::string& sql, const std::vector<SqlValue>& params,
|
||||
std::string& error);
|
||||
|
||||
// Get last insert rowid
|
||||
int64_t GetLastInsertRowId() const;
|
||||
|
||||
// Get affected rows
|
||||
int GetChanges() const;
|
||||
|
||||
bool IsOpen() const { return m_db != nullptr; }
|
||||
void Close();
|
||||
|
||||
private:
|
||||
sqlite3* m_db;
|
||||
std::string m_path;
|
||||
DatabaseLimits m_limits;
|
||||
|
||||
static int Authorizer(void* user_data, int action, const char* arg1,
|
||||
const char* arg2, const char* arg3, const char* arg4);
|
||||
|
||||
bool BindParameters(void* stmt, const std::vector<SqlValue>& params, std::string& error);
|
||||
};
|
||||
|
||||
// Register database.* APIs as globals
|
||||
void RegisterDatabaseAPI(lua_State* L, DatabaseManager* manager);
|
||||
|
||||
} // namespace mosis
|
||||
@@ -1,388 +0,0 @@
|
||||
#include "http_validator.h"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <regex>
|
||||
#include <array>
|
||||
|
||||
namespace mosis {
|
||||
|
||||
HttpValidator::HttpValidator()
|
||||
: m_domain_restrictions_enabled(false)
|
||||
{
|
||||
}
|
||||
|
||||
void HttpValidator::SetAllowedDomains(const std::vector<std::string>& domains) {
|
||||
m_allowed_domains = domains;
|
||||
m_domain_restrictions_enabled = !domains.empty();
|
||||
}
|
||||
|
||||
void HttpValidator::ClearDomainRestrictions() {
|
||||
m_allowed_domains.clear();
|
||||
m_domain_restrictions_enabled = false;
|
||||
}
|
||||
|
||||
std::optional<ParsedUrl> HttpValidator::Validate(const std::string& url, std::string& error) {
|
||||
// Parse URL
|
||||
auto parsed = ParseUrl(url);
|
||||
if (!parsed) {
|
||||
error = "Invalid URL format";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Must be HTTPS or WSS
|
||||
if (parsed->scheme != "https" && parsed->scheme != "wss") {
|
||||
error = "HTTPS or WSS required, got: " + parsed->scheme;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Check for localhost names
|
||||
if (IsLocalhostName(parsed->host)) {
|
||||
error = "localhost blocked for security";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Check for metadata hostnames
|
||||
if (IsMetadataHostname(parsed->host)) {
|
||||
error = "Cloud metadata hostname blocked for security";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Check if it's an IP address and validate
|
||||
if (parsed->is_ip_address) {
|
||||
if (IsBlockedIP(parsed->host)) {
|
||||
error = "IP address blocked: private, localhost, or metadata endpoint";
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
// Check domain whitelist
|
||||
if (m_domain_restrictions_enabled && !IsDomainAllowed(parsed->host)) {
|
||||
error = "Domain not in allowed list: " + parsed->host;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
bool HttpValidator::IsIPv4Address(const std::string& host) {
|
||||
// Simple IPv4 pattern: numbers and dots
|
||||
if (host.empty()) return false;
|
||||
|
||||
int dots = 0;
|
||||
int num_start = 0;
|
||||
for (size_t i = 0; i <= host.length(); i++) {
|
||||
if (i == host.length() || host[i] == '.') {
|
||||
if (i == (size_t)num_start) return false; // Empty segment
|
||||
std::string segment = host.substr(num_start, i - num_start);
|
||||
// Check if segment is a valid number 0-255
|
||||
if (segment.empty() || segment.length() > 3) return false;
|
||||
for (char c : segment) {
|
||||
if (!std::isdigit(static_cast<unsigned char>(c))) return false;
|
||||
}
|
||||
int val = std::stoi(segment);
|
||||
if (val < 0 || val > 255) return false;
|
||||
if (i < host.length()) {
|
||||
dots++;
|
||||
num_start = static_cast<int>(i) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dots == 3;
|
||||
}
|
||||
|
||||
bool HttpValidator::IsIPv6Address(const std::string& host) {
|
||||
// IPv6 addresses in URLs are enclosed in brackets: [::1]
|
||||
if (host.length() < 2) return false;
|
||||
if (host.front() == '[' && host.back() == ']') {
|
||||
return true; // Simplified check - bracket notation means IPv6
|
||||
}
|
||||
// Also check for raw IPv6 (contains colons, no dots or limited dots)
|
||||
int colons = std::count(host.begin(), host.end(), ':');
|
||||
int dots = std::count(host.begin(), host.end(), '.');
|
||||
return colons >= 2 && dots <= 3; // IPv6 has multiple colons
|
||||
}
|
||||
|
||||
bool HttpValidator::IsPrivateIPv4(const std::string& ip) {
|
||||
// Parse IPv4 octets
|
||||
std::array<int, 4> octets{};
|
||||
if (sscanf(ip.c_str(), "%d.%d.%d.%d", &octets[0], &octets[1], &octets[2], &octets[3]) != 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 0.0.0.0 - all interfaces
|
||||
if (octets[0] == 0 && octets[1] == 0 && octets[2] == 0 && octets[3] == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 127.0.0.0/8 - loopback
|
||||
if (octets[0] == 127) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 10.0.0.0/8 - private Class A
|
||||
if (octets[0] == 10) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 172.16.0.0/12 - private Class B (172.16.0.0 - 172.31.255.255)
|
||||
if (octets[0] == 172 && octets[1] >= 16 && octets[1] <= 31) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 192.168.0.0/16 - private Class C
|
||||
if (octets[0] == 192 && octets[1] == 168) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 169.254.0.0/16 - link-local
|
||||
if (octets[0] == 169 && octets[1] == 254) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HttpValidator::IsPrivateIPv6(const std::string& ip) {
|
||||
std::string addr = ip;
|
||||
// Remove brackets if present
|
||||
if (!addr.empty() && addr.front() == '[') addr = addr.substr(1);
|
||||
if (!addr.empty() && addr.back() == ']') addr.pop_back();
|
||||
|
||||
// Convert to lowercase for comparison
|
||||
std::transform(addr.begin(), addr.end(), addr.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
|
||||
// ::1 - loopback
|
||||
if (addr == "::1" || addr == "0:0:0:0:0:0:0:1") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// :: - unspecified (equivalent to 0.0.0.0)
|
||||
if (addr == "::" || addr == "0:0:0:0:0:0:0:0") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// fc00::/7 - unique local addresses (fc00:: to fdff::)
|
||||
if (addr.length() >= 2) {
|
||||
char first = addr[0];
|
||||
char second = addr.length() > 1 ? addr[1] : '0';
|
||||
if (first == 'f' && (second == 'c' || second == 'd')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// fe80::/10 - link-local
|
||||
if (addr.rfind("fe80:", 0) == 0 || addr.rfind("fe8", 0) == 0 ||
|
||||
addr.rfind("fe9", 0) == 0 || addr.rfind("fea", 0) == 0 ||
|
||||
addr.rfind("feb", 0) == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HttpValidator::IsLocalhostIP(const std::string& host) {
|
||||
// IPv4 localhost
|
||||
if (IsIPv4Address(host)) {
|
||||
std::array<int, 4> octets{};
|
||||
if (sscanf(host.c_str(), "%d.%d.%d.%d", &octets[0], &octets[1], &octets[2], &octets[3]) == 4) {
|
||||
return octets[0] == 127;
|
||||
}
|
||||
}
|
||||
|
||||
// IPv6 localhost
|
||||
std::string addr = host;
|
||||
if (!addr.empty() && addr.front() == '[') addr = addr.substr(1);
|
||||
if (!addr.empty() && addr.back() == ']') addr.pop_back();
|
||||
std::transform(addr.begin(), addr.end(), addr.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
|
||||
return addr == "::1" || addr == "0:0:0:0:0:0:0:1";
|
||||
}
|
||||
|
||||
bool HttpValidator::IsMetadataIP(const std::string& host) {
|
||||
// AWS/Azure/GCP metadata endpoint
|
||||
if (host == "169.254.169.254") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// GCP alternate
|
||||
if (host == "metadata.google.internal") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HttpValidator::IsBlockedIP(const std::string& host) {
|
||||
if (IsIPv4Address(host)) {
|
||||
return IsPrivateIPv4(host) || IsMetadataIP(host);
|
||||
}
|
||||
|
||||
if (IsIPv6Address(host)) {
|
||||
return IsPrivateIPv6(host);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HttpValidator::IsDomainAllowed(const std::string& host) {
|
||||
if (!m_domain_restrictions_enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string lower_host = host;
|
||||
std::transform(lower_host.begin(), lower_host.end(), lower_host.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
|
||||
for (const auto& domain : m_allowed_domains) {
|
||||
std::string lower_domain = domain;
|
||||
std::transform(lower_domain.begin(), lower_domain.end(), lower_domain.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
|
||||
// Exact match
|
||||
if (lower_host == lower_domain) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Subdomain match (e.g., "api.example.com" matches "example.com")
|
||||
if (lower_host.length() > lower_domain.length()) {
|
||||
size_t pos = lower_host.length() - lower_domain.length();
|
||||
if (lower_host[pos - 1] == '.' &&
|
||||
lower_host.substr(pos) == lower_domain) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HttpValidator::IsLocalhostName(const std::string& host) {
|
||||
std::string lower = host;
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
|
||||
// Common localhost names
|
||||
if (lower == "localhost") return true;
|
||||
if (lower == "localhost.localdomain") return true;
|
||||
|
||||
// Ends with .localhost
|
||||
if (lower.length() > 10 && lower.substr(lower.length() - 10) == ".localhost") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HttpValidator::IsMetadataHostname(const std::string& host) {
|
||||
std::string lower = host;
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
|
||||
// GCP metadata
|
||||
if (lower == "metadata.google.internal") return true;
|
||||
if (lower == "metadata") return true;
|
||||
|
||||
// Azure metadata
|
||||
if (lower == "metadata.azure.internal") return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<ParsedUrl> HttpValidator::ParseUrl(const std::string& url) {
|
||||
ParsedUrl result;
|
||||
result.port = 443; // Default HTTPS port
|
||||
result.is_ip_address = false;
|
||||
|
||||
// Find scheme
|
||||
size_t scheme_end = url.find("://");
|
||||
if (scheme_end == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
result.scheme = url.substr(0, scheme_end);
|
||||
std::transform(result.scheme.begin(), result.scheme.end(), result.scheme.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
|
||||
// Start of authority
|
||||
size_t auth_start = scheme_end + 3;
|
||||
if (auth_start >= url.length()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Find end of authority (path starts with /)
|
||||
size_t path_start = url.find('/', auth_start);
|
||||
std::string authority;
|
||||
if (path_start == std::string::npos) {
|
||||
authority = url.substr(auth_start);
|
||||
result.path = "/";
|
||||
} else {
|
||||
authority = url.substr(auth_start, path_start - auth_start);
|
||||
|
||||
// Find query string
|
||||
size_t query_start = url.find('?', path_start);
|
||||
if (query_start != std::string::npos) {
|
||||
result.path = url.substr(path_start, query_start - path_start);
|
||||
result.query = url.substr(query_start);
|
||||
} else {
|
||||
result.path = url.substr(path_start);
|
||||
}
|
||||
}
|
||||
|
||||
if (authority.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Parse authority for host and port
|
||||
// Handle IPv6 addresses in brackets
|
||||
if (authority[0] == '[') {
|
||||
size_t bracket_end = authority.find(']');
|
||||
if (bracket_end == std::string::npos) {
|
||||
return std::nullopt; // Malformed IPv6
|
||||
}
|
||||
result.host = authority.substr(0, bracket_end + 1);
|
||||
result.is_ip_address = true;
|
||||
|
||||
// Check for port after bracket
|
||||
if (bracket_end + 1 < authority.length()) {
|
||||
if (authority[bracket_end + 1] == ':') {
|
||||
std::string port_str = authority.substr(bracket_end + 2);
|
||||
try {
|
||||
result.port = static_cast<uint16_t>(std::stoi(port_str));
|
||||
} catch (...) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Regular host or IPv4
|
||||
size_t port_pos = authority.rfind(':');
|
||||
if (port_pos != std::string::npos) {
|
||||
result.host = authority.substr(0, port_pos);
|
||||
std::string port_str = authority.substr(port_pos + 1);
|
||||
try {
|
||||
result.port = static_cast<uint16_t>(std::stoi(port_str));
|
||||
} catch (...) {
|
||||
return std::nullopt;
|
||||
}
|
||||
} else {
|
||||
result.host = authority;
|
||||
}
|
||||
|
||||
// Check if it's an IP address
|
||||
result.is_ip_address = IsIPv4Address(result.host) || IsIPv6Address(result.host);
|
||||
}
|
||||
|
||||
// Default port based on scheme
|
||||
if ((result.scheme == "https" || result.scheme == "wss") && result.port == 0) {
|
||||
result.port = 443;
|
||||
} else if ((result.scheme == "http" || result.scheme == "ws") && result.port == 0) {
|
||||
result.port = 80;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace mosis
|
||||
@@ -1,55 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <cstdint>
|
||||
|
||||
namespace mosis {
|
||||
|
||||
struct ParsedUrl {
|
||||
std::string scheme; // "https"
|
||||
std::string host; // "api.example.com" or "192.0.2.1"
|
||||
uint16_t port; // 443
|
||||
std::string path; // "/api/data"
|
||||
std::string query; // "?key=value"
|
||||
bool is_ip_address; // true if host is IP literal
|
||||
};
|
||||
|
||||
class HttpValidator {
|
||||
public:
|
||||
HttpValidator();
|
||||
|
||||
// Set allowed domains (from app manifest)
|
||||
void SetAllowedDomains(const std::vector<std::string>& domains);
|
||||
|
||||
// Clear domain restrictions (for testing)
|
||||
void ClearDomainRestrictions();
|
||||
|
||||
// Validate URL
|
||||
// Returns parsed URL on success, sets error on failure
|
||||
std::optional<ParsedUrl> Validate(const std::string& url, std::string& error);
|
||||
|
||||
private:
|
||||
std::vector<std::string> m_allowed_domains;
|
||||
bool m_domain_restrictions_enabled;
|
||||
|
||||
// IP address validation
|
||||
bool IsIPv4Address(const std::string& host);
|
||||
bool IsIPv6Address(const std::string& host);
|
||||
bool IsPrivateIPv4(const std::string& ip);
|
||||
bool IsPrivateIPv6(const std::string& ip);
|
||||
bool IsLocalhostIP(const std::string& host);
|
||||
bool IsMetadataIP(const std::string& host);
|
||||
bool IsBlockedIP(const std::string& host);
|
||||
|
||||
// Domain validation
|
||||
bool IsDomainAllowed(const std::string& host);
|
||||
bool IsLocalhostName(const std::string& host);
|
||||
bool IsMetadataHostname(const std::string& host);
|
||||
|
||||
// URL parsing
|
||||
std::optional<ParsedUrl> ParseUrl(const std::string& url);
|
||||
};
|
||||
|
||||
} // namespace mosis
|
||||
@@ -1,369 +0,0 @@
|
||||
#include "json_api.h"
|
||||
|
||||
#include <lua.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <unordered_set>
|
||||
#include <sstream>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace mosis {
|
||||
|
||||
// Registry key for storing limits
|
||||
static const char* JSON_LIMITS_KEY = "__mosis_json_limits";
|
||||
|
||||
// Get limits from registry
|
||||
static JsonLimits GetLimits(lua_State* L) {
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, JSON_LIMITS_KEY);
|
||||
if (lua_islightuserdata(L, -1)) {
|
||||
JsonLimits* limits = static_cast<JsonLimits*>(lua_touserdata(L, -1));
|
||||
lua_pop(L, 1);
|
||||
return *limits;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
return JsonLimits{};
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// JSON DECODE
|
||||
//=============================================================================
|
||||
|
||||
// Custom exception for JSON errors (thrown instead of luaL_error to allow catching)
|
||||
class JsonError : public std::runtime_error {
|
||||
public:
|
||||
explicit JsonError(const std::string& msg) : std::runtime_error(msg) {}
|
||||
};
|
||||
|
||||
// Forward declaration
|
||||
static void JsonToLua(lua_State* L, const json& j, const JsonLimits& limits, int depth);
|
||||
|
||||
static void JsonToLua(lua_State* L, const json& j, const JsonLimits& limits, int depth) {
|
||||
if (depth > limits.max_depth) {
|
||||
throw JsonError("maximum depth exceeded");
|
||||
}
|
||||
|
||||
switch (j.type()) {
|
||||
case json::value_t::null:
|
||||
lua_pushnil(L);
|
||||
break;
|
||||
|
||||
case json::value_t::boolean:
|
||||
lua_pushboolean(L, j.get<bool>() ? 1 : 0);
|
||||
break;
|
||||
|
||||
case json::value_t::number_integer:
|
||||
case json::value_t::number_unsigned:
|
||||
lua_pushinteger(L, j.get<lua_Integer>());
|
||||
break;
|
||||
|
||||
case json::value_t::number_float:
|
||||
lua_pushnumber(L, j.get<lua_Number>());
|
||||
break;
|
||||
|
||||
case json::value_t::string: {
|
||||
const std::string& s = j.get_ref<const std::string&>();
|
||||
if (s.size() > limits.max_string_length) {
|
||||
throw JsonError("string too large");
|
||||
}
|
||||
lua_pushlstring(L, s.c_str(), s.size());
|
||||
break;
|
||||
}
|
||||
|
||||
case json::value_t::array: {
|
||||
if (j.size() > limits.max_array_size) {
|
||||
throw JsonError("array size limit exceeded");
|
||||
}
|
||||
lua_createtable(L, static_cast<int>(j.size()), 0);
|
||||
int i = 1;
|
||||
for (const auto& elem : j) {
|
||||
JsonToLua(L, elem, limits, depth + 1);
|
||||
lua_rawseti(L, -2, i++);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case json::value_t::object: {
|
||||
if (j.size() > limits.max_object_size) {
|
||||
throw JsonError("object size limit exceeded");
|
||||
}
|
||||
lua_createtable(L, 0, static_cast<int>(j.size()));
|
||||
for (auto it = j.begin(); it != j.end(); ++it) {
|
||||
if (it.key().size() > limits.max_string_length) {
|
||||
throw JsonError("key too large");
|
||||
}
|
||||
lua_pushlstring(L, it.key().c_str(), it.key().size());
|
||||
JsonToLua(L, it.value(), limits, depth + 1);
|
||||
lua_rawset(L, -3);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
lua_pushnil(L);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// json.decode(str) -> table|nil, error
|
||||
static int lua_json_decode(lua_State* L) {
|
||||
size_t len;
|
||||
const char* str = luaL_checklstring(L, 1, &len);
|
||||
|
||||
JsonLimits limits = GetLimits(L);
|
||||
|
||||
if (len > limits.max_output_size) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, "input too large");
|
||||
return 2;
|
||||
}
|
||||
|
||||
try {
|
||||
json j = json::parse(str, str + len);
|
||||
JsonToLua(L, j, limits, 0);
|
||||
return 1;
|
||||
} catch (const JsonError& e) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, e.what());
|
||||
return 2;
|
||||
} catch (const json::parse_error& e) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, e.what());
|
||||
return 2;
|
||||
} catch (const std::exception& e) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, e.what());
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// JSON ENCODE
|
||||
//=============================================================================
|
||||
|
||||
// Forward declaration
|
||||
static json LuaToJson(lua_State* L, int index, const JsonLimits& limits,
|
||||
int depth, std::unordered_set<const void*>& visited,
|
||||
size_t& output_size);
|
||||
|
||||
static json LuaToJson(lua_State* L, int index, const JsonLimits& limits,
|
||||
int depth, std::unordered_set<const void*>& visited,
|
||||
size_t& output_size) {
|
||||
if (depth > limits.max_depth) {
|
||||
throw JsonError("maximum depth exceeded");
|
||||
}
|
||||
|
||||
if (output_size > limits.max_output_size) {
|
||||
throw JsonError("output size limit exceeded");
|
||||
}
|
||||
|
||||
int type = lua_type(L, index);
|
||||
|
||||
switch (type) {
|
||||
case LUA_TNIL:
|
||||
return nullptr;
|
||||
|
||||
case LUA_TBOOLEAN:
|
||||
return lua_toboolean(L, index) != 0;
|
||||
|
||||
case LUA_TNUMBER:
|
||||
if (lua_isinteger(L, index)) {
|
||||
return lua_tointeger(L, index);
|
||||
}
|
||||
return lua_tonumber(L, index);
|
||||
|
||||
case LUA_TSTRING: {
|
||||
size_t len;
|
||||
const char* s = lua_tolstring(L, index, &len);
|
||||
if (len > limits.max_string_length) {
|
||||
throw JsonError("string too large");
|
||||
}
|
||||
output_size += len + 2; // Approximate: string + quotes
|
||||
return std::string(s, len);
|
||||
}
|
||||
|
||||
case LUA_TTABLE: {
|
||||
// Check for cycles
|
||||
const void* ptr = lua_topointer(L, index);
|
||||
if (visited.find(ptr) != visited.end()) {
|
||||
throw JsonError("circular reference detected");
|
||||
}
|
||||
visited.insert(ptr);
|
||||
|
||||
// Determine if array or object by checking keys
|
||||
bool is_array = true;
|
||||
size_t array_len = 0;
|
||||
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, index) != 0) {
|
||||
if (lua_type(L, -2) == LUA_TNUMBER && lua_isinteger(L, -2)) {
|
||||
lua_Integer key = lua_tointeger(L, -2);
|
||||
if (key >= 1) {
|
||||
array_len = std::max(array_len, static_cast<size_t>(key));
|
||||
} else {
|
||||
is_array = false;
|
||||
}
|
||||
} else {
|
||||
is_array = false;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
// Verify array is contiguous
|
||||
if (is_array && array_len > 0) {
|
||||
for (size_t i = 1; i <= array_len; i++) {
|
||||
lua_rawgeti(L, index, static_cast<int>(i));
|
||||
if (lua_isnil(L, -1)) {
|
||||
is_array = false;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (!is_array) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array && array_len > 0) {
|
||||
if (array_len > limits.max_array_size) {
|
||||
throw JsonError("array size limit exceeded");
|
||||
}
|
||||
json arr = json::array();
|
||||
for (size_t i = 1; i <= array_len; i++) {
|
||||
lua_rawgeti(L, index, static_cast<int>(i));
|
||||
arr.push_back(LuaToJson(L, lua_gettop(L), limits, depth + 1, visited, output_size));
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
visited.erase(ptr);
|
||||
return arr;
|
||||
} else {
|
||||
// Object
|
||||
json obj = json::object();
|
||||
size_t key_count = 0;
|
||||
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, index) != 0) {
|
||||
key_count++;
|
||||
if (key_count > limits.max_object_size) {
|
||||
throw JsonError("object size limit exceeded");
|
||||
}
|
||||
|
||||
// Get key as string
|
||||
std::string key;
|
||||
if (lua_type(L, -2) == LUA_TSTRING) {
|
||||
size_t len;
|
||||
const char* s = lua_tolstring(L, -2, &len);
|
||||
key = std::string(s, len);
|
||||
} else if (lua_type(L, -2) == LUA_TNUMBER) {
|
||||
if (lua_isinteger(L, -2)) {
|
||||
key = std::to_string(lua_tointeger(L, -2));
|
||||
} else {
|
||||
key = std::to_string(lua_tonumber(L, -2));
|
||||
}
|
||||
} else {
|
||||
lua_pop(L, 2);
|
||||
throw JsonError("unsupported key type");
|
||||
}
|
||||
|
||||
output_size += key.size() + 3; // key + quotes + colon
|
||||
|
||||
obj[key] = LuaToJson(L, lua_gettop(L), limits, depth + 1, visited, output_size);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
visited.erase(ptr);
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
case LUA_TFUNCTION:
|
||||
case LUA_TUSERDATA:
|
||||
case LUA_TTHREAD:
|
||||
case LUA_TLIGHTUSERDATA:
|
||||
throw JsonError(std::string("unsupported type '") + lua_typename(L, type) + "'");
|
||||
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// json.encode(table) -> string|nil, error
|
||||
static int lua_json_encode(lua_State* L) {
|
||||
luaL_checktype(L, 1, LUA_TTABLE);
|
||||
|
||||
JsonLimits limits = GetLimits(L);
|
||||
std::unordered_set<const void*> visited;
|
||||
size_t output_size = 0;
|
||||
|
||||
try {
|
||||
json j = LuaToJson(L, 1, limits, 0, visited, output_size);
|
||||
std::string result = j.dump();
|
||||
|
||||
if (result.size() > limits.max_output_size) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, "output size limit exceeded");
|
||||
return 2;
|
||||
}
|
||||
|
||||
lua_pushlstring(L, result.c_str(), result.size());
|
||||
return 1;
|
||||
} catch (const JsonError& e) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, e.what());
|
||||
return 2;
|
||||
} catch (const std::exception& e) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, e.what());
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// REGISTRATION
|
||||
//=============================================================================
|
||||
|
||||
// Helper to set a global in the real _G (bypassing any proxy)
|
||||
static void SetGlobalInRealG(lua_State* L, const char* name) {
|
||||
// Stack: value to set as global
|
||||
|
||||
// Get _G (might be a proxy)
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
|
||||
|
||||
// Check if it has a metatable with __index (proxy pattern)
|
||||
if (lua_getmetatable(L, -1)) {
|
||||
lua_getfield(L, -1, "__index");
|
||||
if (lua_istable(L, -1)) {
|
||||
// Found real _G through proxy's __index
|
||||
// Stack: value, proxy, mt, real_G
|
||||
lua_pushvalue(L, -4); // Copy value
|
||||
lua_setfield(L, -2, name); // real_G[name] = value
|
||||
lua_pop(L, 4); // pop real_G, mt, proxy, original value
|
||||
return;
|
||||
}
|
||||
lua_pop(L, 2); // pop __index, metatable
|
||||
}
|
||||
|
||||
// No proxy, set directly in _G
|
||||
// Stack: value, _G
|
||||
lua_pushvalue(L, -2); // Copy value
|
||||
lua_setfield(L, -2, name); // _G[name] = value
|
||||
lua_pop(L, 2); // pop _G, original value
|
||||
}
|
||||
|
||||
void RegisterJsonAPI(lua_State* L, const JsonLimits& limits) {
|
||||
// Store limits in registry (allocate static storage)
|
||||
static JsonLimits stored_limits;
|
||||
stored_limits = limits;
|
||||
lua_pushlightuserdata(L, &stored_limits);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, JSON_LIMITS_KEY);
|
||||
|
||||
// Create json table
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushcfunction(L, lua_json_decode);
|
||||
lua_setfield(L, -2, "decode");
|
||||
|
||||
lua_pushcfunction(L, lua_json_encode);
|
||||
lua_setfield(L, -2, "encode");
|
||||
|
||||
// Set as global (bypassing proxy)
|
||||
SetGlobalInRealG(L, "json");
|
||||
}
|
||||
|
||||
} // namespace mosis
|
||||
@@ -1,22 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <cstddef>
|
||||
|
||||
struct lua_State;
|
||||
|
||||
namespace mosis {
|
||||
|
||||
// Configuration limits for JSON operations
|
||||
struct JsonLimits {
|
||||
int max_depth = 32; // Maximum nesting depth
|
||||
size_t max_string_length = 1 * 1024 * 1024; // 1 MB per string
|
||||
size_t max_output_size = 10 * 1024 * 1024; // 10 MB total output
|
||||
size_t max_array_size = 100000; // Max elements in array
|
||||
size_t max_object_size = 10000; // Max keys in object
|
||||
};
|
||||
|
||||
// Register json.encode() and json.decode() as globals
|
||||
void RegisterJsonAPI(lua_State* L, const JsonLimits& limits = JsonLimits{});
|
||||
|
||||
} // namespace mosis
|
||||
@@ -1,448 +0,0 @@
|
||||
#include "lua_sandbox.h"
|
||||
|
||||
#include <lua.hpp>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace mosis {
|
||||
|
||||
//=============================================================================
|
||||
// ALLOCATOR
|
||||
//=============================================================================
|
||||
|
||||
void* LuaSandbox::SandboxAlloc(void* ud, void* ptr, size_t osize, size_t nsize) {
|
||||
auto* sandbox = static_cast<LuaSandbox*>(ud);
|
||||
|
||||
// Calculate new usage
|
||||
// osize is the old size (0 for new allocations)
|
||||
// nsize is the new size (0 for frees)
|
||||
size_t new_usage = sandbox->m_memory_used - osize + nsize;
|
||||
|
||||
// Check limit (only when allocating, not freeing)
|
||||
if (nsize > 0 && new_usage > sandbox->m_limits.memory_bytes) {
|
||||
// Allocation would exceed limit - return nullptr to signal failure
|
||||
// Lua will raise a memory error
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Update tracking
|
||||
sandbox->m_memory_used = new_usage;
|
||||
|
||||
// Free operation
|
||||
if (nsize == 0) {
|
||||
free(ptr);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Alloc or realloc
|
||||
return realloc(ptr, nsize);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// INSTRUCTION HOOK
|
||||
//=============================================================================
|
||||
|
||||
void LuaSandbox::InstructionHook(lua_State* L, lua_Debug* ar) {
|
||||
(void)ar; // Unused
|
||||
|
||||
// Get sandbox pointer from registry
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "__mosis_sandbox");
|
||||
auto* sandbox = static_cast<LuaSandbox*>(lua_touserdata(L, -1));
|
||||
lua_pop(L, 1);
|
||||
|
||||
if (!sandbox) return;
|
||||
|
||||
// Increment by hook interval (called every 1000 instructions)
|
||||
sandbox->m_instructions_used += 1000;
|
||||
|
||||
// Check limit
|
||||
if (sandbox->m_instructions_used > sandbox->m_limits.instructions_per_call) {
|
||||
luaL_error(L, "instruction limit exceeded");
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// SAFE PRINT
|
||||
//=============================================================================
|
||||
|
||||
int LuaSandbox::SafePrint(lua_State* L) {
|
||||
int n = lua_gettop(L); // number of arguments
|
||||
lua_getglobal(L, "tostring");
|
||||
|
||||
for (int i = 1; i <= n; i++) {
|
||||
if (i > 1) std::cout << "\t";
|
||||
|
||||
lua_pushvalue(L, -1); // push tostring
|
||||
lua_pushvalue(L, i); // push argument
|
||||
lua_call(L, 1, 1); // call tostring
|
||||
|
||||
const char* s = lua_tostring(L, -1);
|
||||
if (s) {
|
||||
std::cout << s;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
std::cout << std::endl;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// CONSTRUCTOR / DESTRUCTOR
|
||||
//=============================================================================
|
||||
|
||||
LuaSandbox::LuaSandbox(const SandboxContext& context, const SandboxLimits& limits)
|
||||
: m_context(context), m_limits(limits) {
|
||||
|
||||
// Create Lua state with custom allocator
|
||||
m_L = lua_newstate(SandboxAlloc, this);
|
||||
if (!m_L) {
|
||||
m_last_error = "Failed to create Lua state";
|
||||
return;
|
||||
}
|
||||
|
||||
// Store sandbox pointer in registry for hooks to access
|
||||
lua_pushlightuserdata(m_L, this);
|
||||
lua_setfield(m_L, LUA_REGISTRYINDEX, "__mosis_sandbox");
|
||||
|
||||
// Setup the sandbox
|
||||
SetupSandbox();
|
||||
}
|
||||
|
||||
LuaSandbox::~LuaSandbox() {
|
||||
if (m_L) {
|
||||
lua_close(m_L);
|
||||
m_L = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// SETUP
|
||||
//=============================================================================
|
||||
|
||||
void LuaSandbox::SetupSandbox() {
|
||||
// Open safe standard libraries
|
||||
luaL_openlibs(m_L);
|
||||
|
||||
// Remove dangerous globals FIRST
|
||||
RemoveDangerousGlobals();
|
||||
|
||||
// Setup safe replacements
|
||||
SetupSafeGlobals();
|
||||
|
||||
// Protect metatables
|
||||
ProtectBuiltinTables();
|
||||
|
||||
// Setup instruction hook for CPU limiting
|
||||
SetupInstructionHook();
|
||||
}
|
||||
|
||||
void LuaSandbox::RemoveDangerousGlobals() {
|
||||
// List of dangerous globals to remove
|
||||
const char* dangerous_globals[] = {
|
||||
// Code execution from files/strings
|
||||
"dofile",
|
||||
"loadfile",
|
||||
"load",
|
||||
"loadstring", // Lua 5.1 compat
|
||||
|
||||
// Raw access (bypasses metatables)
|
||||
"rawget",
|
||||
"rawset",
|
||||
"rawequal",
|
||||
"rawlen",
|
||||
|
||||
// Metatable manipulation
|
||||
// Note: We keep getmetatable but protect the actual metatables
|
||||
// setmetatable is removed to prevent modifications
|
||||
"setmetatable",
|
||||
|
||||
// GC manipulation
|
||||
"collectgarbage",
|
||||
|
||||
// Dangerous libraries
|
||||
"os",
|
||||
"io",
|
||||
"debug",
|
||||
"package",
|
||||
|
||||
// LuaJIT / FFI (if present)
|
||||
"ffi",
|
||||
"jit",
|
||||
"newproxy",
|
||||
|
||||
// Module system (we'll add safe version later)
|
||||
"require",
|
||||
|
||||
nullptr
|
||||
};
|
||||
|
||||
for (const char** p = dangerous_globals; *p; ++p) {
|
||||
lua_pushnil(m_L);
|
||||
lua_setglobal(m_L, *p);
|
||||
}
|
||||
|
||||
// Remove string.dump (can create bytecode from functions)
|
||||
lua_getglobal(m_L, "string");
|
||||
if (lua_istable(m_L, -1)) {
|
||||
lua_pushnil(m_L);
|
||||
lua_setfield(m_L, -2, "dump");
|
||||
}
|
||||
lua_pop(m_L, 1);
|
||||
}
|
||||
|
||||
void LuaSandbox::SetupSafeGlobals() {
|
||||
// Replace print with safe version
|
||||
lua_pushcfunction(m_L, SafePrint);
|
||||
lua_setglobal(m_L, "print");
|
||||
|
||||
// Setup safe require if app_path is set
|
||||
if (!m_context.app_path.empty()) {
|
||||
SetupSafeRequire();
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// SAFE REQUIRE
|
||||
//=============================================================================
|
||||
|
||||
// Registry key for loaded modules cache
|
||||
static const char* LOADED_KEY = "mosis.loaded_modules";
|
||||
|
||||
// Validate module name for require() - alphanumeric, underscore, dots only
|
||||
static bool IsValidModuleName(const std::string& name) {
|
||||
if (name.empty()) return false;
|
||||
|
||||
for (size_t i = 0; i < name.length(); i++) {
|
||||
char c = name[i];
|
||||
if (std::isalnum(static_cast<unsigned char>(c))) continue;
|
||||
if (c == '_') continue;
|
||||
if (c == '.') {
|
||||
if (i == 0 || i == name.length() - 1) return false;
|
||||
if (i > 0 && name[i-1] == '.') return false;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name.find("..") != std::string::npos) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Convert module name to path (e.g., "ui.button" -> "scripts/ui/button.lua")
|
||||
static std::string ModuleToPath(const std::string& module_name) {
|
||||
std::string path = module_name;
|
||||
std::replace(path.begin(), path.end(), '.', '/');
|
||||
return "scripts/" + path + ".lua";
|
||||
}
|
||||
|
||||
int LuaSandbox::SafeRequire(lua_State* L) {
|
||||
// Get module name
|
||||
const char* module_name = luaL_checkstring(L, 1);
|
||||
|
||||
// Validate module name
|
||||
if (!IsValidModuleName(module_name)) {
|
||||
return luaL_error(L, "invalid module name: %s", module_name);
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_getfield(L, -1, module_name);
|
||||
if (!lua_isnil(L, -1)) {
|
||||
return 1; // Return cached module
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Get sandbox pointer from registry
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "__mosis_sandbox");
|
||||
auto* sandbox = static_cast<LuaSandbox*>(lua_touserdata(L, -1));
|
||||
lua_pop(L, 1);
|
||||
|
||||
if (!sandbox) {
|
||||
return luaL_error(L, "require not properly initialized");
|
||||
}
|
||||
|
||||
// Build full path
|
||||
std::string relative_path = ModuleToPath(module_name);
|
||||
std::string full_path = sandbox->m_context.app_path;
|
||||
if (!full_path.empty() && full_path.back() != '/' && full_path.back() != '\\') {
|
||||
full_path += '/';
|
||||
}
|
||||
full_path += relative_path;
|
||||
|
||||
// Read the file
|
||||
std::ifstream file(full_path);
|
||||
if (!file.is_open()) {
|
||||
return luaL_error(L, "module '%s' not found at '%s'", module_name, full_path.c_str());
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
std::string source = buffer.str();
|
||||
file.close();
|
||||
|
||||
// Load as text only (no bytecode)
|
||||
std::string chunk_name = "@" + std::string(module_name);
|
||||
int status = luaL_loadbufferx(L, source.c_str(), source.size(),
|
||||
chunk_name.c_str(), "t");
|
||||
|
||||
if (status != LUA_OK) {
|
||||
return lua_error(L);
|
||||
}
|
||||
|
||||
// Execute the chunk
|
||||
lua_call(L, 0, 1);
|
||||
|
||||
// If chunk returned nil, use true as the module value
|
||||
if (lua_isnil(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
lua_pushboolean(L, 1);
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
|
||||
if (!lua_istable(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
lua_newtable(L);
|
||||
lua_pushvalue(L, -1);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
|
||||
}
|
||||
lua_pushvalue(L, -2);
|
||||
lua_setfield(L, -2, module_name);
|
||||
lua_pop(L, 1);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
void LuaSandbox::SetupSafeRequire() {
|
||||
// Create loaded modules cache
|
||||
lua_newtable(m_L);
|
||||
lua_setfield(m_L, LUA_REGISTRYINDEX, LOADED_KEY);
|
||||
|
||||
// Register require function
|
||||
lua_pushcfunction(m_L, SafeRequire);
|
||||
lua_setglobal(m_L, "require");
|
||||
}
|
||||
|
||||
void LuaSandbox::ProtectBuiltinTables() {
|
||||
// Protect string metatable
|
||||
// When someone calls getmetatable(""), they get "string" instead of the real metatable
|
||||
lua_pushstring(m_L, "");
|
||||
if (lua_getmetatable(m_L, -1)) {
|
||||
lua_pushstring(m_L, "string");
|
||||
lua_setfield(m_L, -2, "__metatable");
|
||||
lua_pop(m_L, 1); // pop metatable
|
||||
}
|
||||
lua_pop(m_L, 1); // pop string
|
||||
|
||||
// Freeze _G using a proxy pattern
|
||||
// This is needed because __newindex only fires for NEW keys, not existing ones
|
||||
// We create: empty_proxy -> metatable { __index = real_G, __newindex = error }
|
||||
|
||||
// Get the current _G (with all our safe functions)
|
||||
lua_pushglobaltable(m_L); // stack: real_G
|
||||
|
||||
// Create a new empty table to be the proxy
|
||||
lua_newtable(m_L); // stack: real_G, proxy
|
||||
|
||||
// Create metatable for proxy
|
||||
lua_newtable(m_L); // stack: real_G, proxy, mt
|
||||
|
||||
// __metatable - prevent access to real metatable
|
||||
lua_pushstring(m_L, "globals");
|
||||
lua_setfield(m_L, -2, "__metatable");
|
||||
|
||||
// __index - read from real_G
|
||||
lua_pushvalue(m_L, -3); // push real_G
|
||||
lua_setfield(m_L, -2, "__index");
|
||||
|
||||
// __newindex - block all writes
|
||||
lua_pushcfunction(m_L, [](lua_State* L) -> int {
|
||||
const char* key = lua_tostring(L, 2);
|
||||
return luaL_error(L, "cannot modify global '%s'", key ? key : "(unknown)");
|
||||
});
|
||||
lua_setfield(m_L, -2, "__newindex");
|
||||
|
||||
// Set metatable on proxy: setmetatable(proxy, mt)
|
||||
lua_setmetatable(m_L, -2); // stack: real_G, proxy
|
||||
|
||||
// Now we need to replace _G with proxy
|
||||
// In Lua 5.2+, we use lua_rawseti on the registry
|
||||
lua_pushvalue(m_L, -1); // stack: real_G, proxy, proxy
|
||||
lua_rawseti(m_L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS); // stack: real_G, proxy
|
||||
|
||||
// Also update _G variable in real_G to point to proxy
|
||||
// This is critical: when code does _G.foo = bar, it accesses _G variable
|
||||
lua_setfield(m_L, -2, "_G"); // stack: real_G (sets real_G["_G"] = proxy)
|
||||
|
||||
lua_pop(m_L, 1); // pop real_G
|
||||
}
|
||||
|
||||
void LuaSandbox::SetupInstructionHook() {
|
||||
// Set hook to fire every 1000 VM instructions
|
||||
lua_sethook(m_L, InstructionHook, LUA_MASKCOUNT, 1000);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// LOAD AND EXECUTE
|
||||
//=============================================================================
|
||||
|
||||
bool LuaSandbox::LoadString(const std::string& code, const std::string& chunk_name) {
|
||||
if (!m_L) {
|
||||
m_last_error = "Lua state not initialized";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reset instruction count for this execution
|
||||
ResetInstructionCount();
|
||||
|
||||
// Load as TEXT ONLY - "t" mode rejects bytecode (starts with \x1bLua)
|
||||
int result = luaL_loadbufferx(m_L, code.c_str(), code.size(),
|
||||
chunk_name.c_str(), "t");
|
||||
|
||||
if (result != LUA_OK) {
|
||||
m_last_error = lua_tostring(m_L, -1);
|
||||
lua_pop(m_L, 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Execute the loaded chunk
|
||||
result = lua_pcall(m_L, 0, 0, 0);
|
||||
if (result != LUA_OK) {
|
||||
m_last_error = lua_tostring(m_L, -1);
|
||||
lua_pop(m_L, 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_last_error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LuaSandbox::LoadFile(const std::string& path) {
|
||||
// Read file contents
|
||||
std::ifstream f(path);
|
||||
if (!f) {
|
||||
m_last_error = "Cannot open file: " + path;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::stringstream ss;
|
||||
ss << f.rdbuf();
|
||||
std::string code = ss.str();
|
||||
|
||||
// Load as string
|
||||
return LoadString(code, "@" + path);
|
||||
}
|
||||
|
||||
void LuaSandbox::ResetInstructionCount() {
|
||||
m_instructions_used = 0;
|
||||
}
|
||||
|
||||
} // namespace mosis
|
||||
@@ -1,101 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
// Forward declare lua_State to avoid including lua.h in header
|
||||
struct lua_State;
|
||||
struct lua_Debug;
|
||||
|
||||
namespace mosis {
|
||||
|
||||
// Resource limits for sandbox
|
||||
struct SandboxLimits {
|
||||
size_t memory_bytes = 16 * 1024 * 1024; // 16 MB default
|
||||
size_t max_string_size = 1 * 1024 * 1024; // 1 MB max string
|
||||
size_t max_table_entries = 100000; // Prevent hash DoS
|
||||
uint64_t instructions_per_call = 1000000; // ~10ms execution
|
||||
int stack_depth = 200; // Recursion limit
|
||||
};
|
||||
|
||||
// Context for sandbox (app identity, permissions, etc.)
|
||||
struct SandboxContext {
|
||||
std::string app_id;
|
||||
std::string app_path;
|
||||
std::vector<std::string> permissions;
|
||||
bool is_system_app = false;
|
||||
};
|
||||
|
||||
// Isolated Lua execution environment
|
||||
class LuaSandbox {
|
||||
public:
|
||||
explicit LuaSandbox(const SandboxContext& context,
|
||||
const SandboxLimits& limits = {});
|
||||
~LuaSandbox();
|
||||
|
||||
// Non-copyable, non-movable
|
||||
LuaSandbox(const LuaSandbox&) = delete;
|
||||
LuaSandbox& operator=(const LuaSandbox&) = delete;
|
||||
LuaSandbox(LuaSandbox&&) = delete;
|
||||
LuaSandbox& operator=(LuaSandbox&&) = delete;
|
||||
|
||||
// Load and execute Lua code (text only, bytecode rejected)
|
||||
bool LoadString(const std::string& code, const std::string& chunk_name = "chunk");
|
||||
bool LoadFile(const std::string& path);
|
||||
|
||||
// State access
|
||||
lua_State* GetState() const { return m_L; }
|
||||
const std::string& GetLastError() const { return m_last_error; }
|
||||
|
||||
// Resource usage
|
||||
size_t GetMemoryUsed() const { return m_memory_used; }
|
||||
uint64_t GetInstructionsUsed() const { return m_instructions_used; }
|
||||
|
||||
// Context access
|
||||
const SandboxContext& GetContext() const { return m_context; }
|
||||
const SandboxLimits& GetLimits() const { return m_limits; }
|
||||
const std::string& app_id() const { return m_context.app_id; }
|
||||
|
||||
// Reset instruction counter (call before each event handler)
|
||||
void ResetInstructionCount();
|
||||
|
||||
// Check if sandbox is in valid state
|
||||
bool IsValid() const { return m_L != nullptr; }
|
||||
|
||||
private:
|
||||
// Setup functions
|
||||
void SetupSandbox();
|
||||
void RemoveDangerousGlobals();
|
||||
void ProtectBuiltinTables();
|
||||
void SetupInstructionHook();
|
||||
void SetupSafeGlobals();
|
||||
void SetupSafeRequire();
|
||||
|
||||
// Allocator callback (static for C compatibility)
|
||||
static void* SandboxAlloc(void* ud, void* ptr, size_t osize, size_t nsize);
|
||||
|
||||
// Instruction hook callback (static for C compatibility)
|
||||
static void InstructionHook(lua_State* L, lua_Debug* ar);
|
||||
|
||||
// Safe print function
|
||||
static int SafePrint(lua_State* L);
|
||||
|
||||
// Safe require function
|
||||
static int SafeRequire(lua_State* L);
|
||||
|
||||
lua_State* m_L = nullptr;
|
||||
SandboxContext m_context;
|
||||
SandboxLimits m_limits;
|
||||
|
||||
size_t m_memory_used = 0;
|
||||
uint64_t m_instructions_used = 0;
|
||||
std::string m_last_error;
|
||||
};
|
||||
|
||||
} // namespace mosis
|
||||
|
||||
// Convenience alias for tests
|
||||
using SandboxContext = mosis::SandboxContext;
|
||||
using SandboxLimits = mosis::SandboxLimits;
|
||||
using LuaSandbox = mosis::LuaSandbox;
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "microphone_interface.h"
|
||||
#include "permission_gate.h"
|
||||
#include <mosis/sandbox/permission_gate.h>
|
||||
#include <lua.hpp>
|
||||
|
||||
namespace mosis {
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
#include "network_manager.h"
|
||||
#include <lua.hpp>
|
||||
#include <algorithm>
|
||||
|
||||
namespace mosis {
|
||||
|
||||
NetworkManager::NetworkManager(const std::string& app_id, const NetworkLimits& limits)
|
||||
: m_app_id(app_id)
|
||||
, m_limits(limits)
|
||||
, m_mock_mode(true)
|
||||
{
|
||||
}
|
||||
|
||||
NetworkManager::~NetworkManager() {
|
||||
}
|
||||
|
||||
void NetworkManager::SetAllowedDomains(const std::vector<std::string>& domains) {
|
||||
m_validator.SetAllowedDomains(domains);
|
||||
}
|
||||
|
||||
void NetworkManager::ClearDomainRestrictions() {
|
||||
m_validator.ClearDomainRestrictions();
|
||||
}
|
||||
|
||||
bool NetworkManager::ValidateRequest(const HttpRequest& request, std::string& error) {
|
||||
// Validate URL
|
||||
auto parsed = m_validator.Validate(request.url, error);
|
||||
if (!parsed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate method
|
||||
std::string method = request.method;
|
||||
std::transform(method.begin(), method.end(), method.begin(),
|
||||
[](unsigned char c) { return std::toupper(c); });
|
||||
|
||||
static const std::vector<std::string> allowed_methods = {
|
||||
"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"
|
||||
};
|
||||
|
||||
bool method_valid = false;
|
||||
for (const auto& m : allowed_methods) {
|
||||
if (method == m) {
|
||||
method_valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!method_valid) {
|
||||
error = "Invalid HTTP method: " + request.method;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate request body size
|
||||
if (request.body.size() > m_limits.max_request_body) {
|
||||
error = "Request body too large: " + std::to_string(request.body.size()) +
|
||||
" bytes (max " + std::to_string(m_limits.max_request_body) + ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate timeout
|
||||
if (request.timeout_ms > m_limits.max_timeout_ms) {
|
||||
error = "Timeout too large: " + std::to_string(request.timeout_ms) +
|
||||
"ms (max " + std::to_string(m_limits.max_timeout_ms) + "ms)";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check concurrent request limit
|
||||
if (m_active_requests.load() >= m_limits.max_concurrent_requests) {
|
||||
error = "Too many concurrent requests (max " +
|
||||
std::to_string(m_limits.max_concurrent_requests) + ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
HttpResponse NetworkManager::Request(const HttpRequest& request, std::string& error) {
|
||||
HttpResponse response;
|
||||
|
||||
// Validate the request
|
||||
if (!ValidateRequest(request, error)) {
|
||||
response.error = error;
|
||||
return response;
|
||||
}
|
||||
|
||||
// In mock mode, we don't actually make network calls
|
||||
// This is for testing the validation logic
|
||||
if (m_mock_mode) {
|
||||
error = "Network requests disabled in mock mode";
|
||||
response.error = error;
|
||||
return response;
|
||||
}
|
||||
|
||||
// Track active requests
|
||||
m_active_requests++;
|
||||
|
||||
// In a real implementation, we would make the HTTP request here
|
||||
// For now, just return an error indicating no network implementation
|
||||
error = "Network requests not implemented on this platform";
|
||||
response.error = error;
|
||||
|
||||
m_active_requests--;
|
||||
return response;
|
||||
}
|
||||
|
||||
int NetworkManager::GetActiveRequestCount() const {
|
||||
return m_active_requests.load();
|
||||
}
|
||||
|
||||
// Lua API implementation
|
||||
|
||||
// Get NetworkManager from upvalue
|
||||
static NetworkManager* GetManager(lua_State* L) {
|
||||
return static_cast<NetworkManager*>(lua_touserdata(L, lua_upvalueindex(1)));
|
||||
}
|
||||
|
||||
// network.request(options) -> response, error
|
||||
static int L_network_request(lua_State* L) {
|
||||
NetworkManager* manager = GetManager(L);
|
||||
if (!manager) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, "NetworkManager not available");
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Expect table argument
|
||||
luaL_checktype(L, 1, LUA_TTABLE);
|
||||
|
||||
HttpRequest request;
|
||||
|
||||
// Get URL (required)
|
||||
lua_getfield(L, 1, "url");
|
||||
if (!lua_isstring(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, "url is required and must be a string");
|
||||
return 2;
|
||||
}
|
||||
request.url = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Get method (optional, default GET)
|
||||
lua_getfield(L, 1, "method");
|
||||
if (lua_isstring(L, -1)) {
|
||||
request.method = lua_tostring(L, -1);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Get headers (optional)
|
||||
lua_getfield(L, 1, "headers");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_isstring(L, -1)) {
|
||||
request.headers[lua_tostring(L, -2)] = lua_tostring(L, -1);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Get body (optional)
|
||||
lua_getfield(L, 1, "body");
|
||||
if (lua_isstring(L, -1)) {
|
||||
size_t len;
|
||||
const char* body = lua_tolstring(L, -1, &len);
|
||||
request.body = std::string(body, len);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Get timeout (optional)
|
||||
lua_getfield(L, 1, "timeout");
|
||||
if (lua_isnumber(L, -1)) {
|
||||
request.timeout_ms = static_cast<int>(lua_tointeger(L, -1));
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Make request
|
||||
std::string error;
|
||||
HttpResponse response = manager->Request(request, error);
|
||||
|
||||
if (!error.empty()) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, error.c_str());
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Return response as table
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushinteger(L, response.status_code);
|
||||
lua_setfield(L, -2, "status");
|
||||
|
||||
lua_pushstring(L, response.body.c_str());
|
||||
lua_setfield(L, -2, "body");
|
||||
|
||||
// Headers table
|
||||
lua_newtable(L);
|
||||
for (const auto& [key, value] : response.headers) {
|
||||
lua_pushstring(L, value.c_str());
|
||||
lua_setfield(L, -2, key.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "headers");
|
||||
|
||||
if (!response.error.empty()) {
|
||||
lua_pushstring(L, response.error.c_str());
|
||||
lua_setfield(L, -2, "error");
|
||||
}
|
||||
|
||||
return 1; // Return response table
|
||||
}
|
||||
|
||||
// Helper to set a global in the real _G (bypassing any proxy)
|
||||
static void SetGlobalInRealG(lua_State* L, const char* name) {
|
||||
// Stack: value to set as global
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
|
||||
|
||||
// Check if it's a proxy with __index pointing to real _G
|
||||
if (lua_getmetatable(L, -1)) {
|
||||
lua_getfield(L, -1, "__index");
|
||||
if (lua_istable(L, -1)) {
|
||||
// This is the real _G, set our value there
|
||||
lua_pushvalue(L, -4); // Push the value
|
||||
lua_setfield(L, -2, name);
|
||||
lua_pop(L, 4); // Pop __index, metatable, proxy, (value already consumed)
|
||||
return;
|
||||
}
|
||||
lua_pop(L, 2); // Pop __index and metatable
|
||||
}
|
||||
|
||||
// No proxy, set directly
|
||||
lua_pushvalue(L, -2); // Push the value
|
||||
lua_setfield(L, -2, name);
|
||||
lua_pop(L, 2); // Pop globals table and original value
|
||||
}
|
||||
|
||||
void RegisterNetworkAPI(lua_State* L, NetworkManager* manager) {
|
||||
// Create network table
|
||||
lua_newtable(L);
|
||||
|
||||
// Add request function with manager as upvalue
|
||||
lua_pushlightuserdata(L, manager);
|
||||
lua_pushcclosure(L, L_network_request, 1);
|
||||
lua_setfield(L, -2, "request");
|
||||
|
||||
// Set as global
|
||||
SetGlobalInRealG(L, "network");
|
||||
}
|
||||
|
||||
} // namespace mosis
|
||||
@@ -1,76 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include "http_validator.h"
|
||||
|
||||
struct lua_State;
|
||||
|
||||
namespace mosis {
|
||||
|
||||
struct HttpRequest {
|
||||
std::string url;
|
||||
std::string method = "GET";
|
||||
std::map<std::string, std::string> headers;
|
||||
std::string body;
|
||||
int timeout_ms = 30000;
|
||||
};
|
||||
|
||||
struct HttpResponse {
|
||||
int status_code = 0;
|
||||
std::map<std::string, std::string> headers;
|
||||
std::string body;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
struct NetworkLimits {
|
||||
size_t max_request_body = 10 * 1024 * 1024; // 10 MB
|
||||
size_t max_response_body = 50 * 1024 * 1024; // 50 MB
|
||||
int max_timeout_ms = 60000; // 60 seconds
|
||||
int max_concurrent_requests = 6;
|
||||
int default_timeout_ms = 30000;
|
||||
};
|
||||
|
||||
class NetworkManager {
|
||||
public:
|
||||
NetworkManager(const std::string& app_id, const NetworkLimits& limits = NetworkLimits{});
|
||||
~NetworkManager();
|
||||
|
||||
// Configure domain restrictions
|
||||
void SetAllowedDomains(const std::vector<std::string>& domains);
|
||||
void ClearDomainRestrictions();
|
||||
|
||||
// Synchronous request
|
||||
// In test mode, validates but doesn't actually make network calls
|
||||
HttpResponse Request(const HttpRequest& request, std::string& error);
|
||||
|
||||
// Stats
|
||||
int GetActiveRequestCount() const;
|
||||
|
||||
// Access validator for testing
|
||||
HttpValidator& GetValidator() { return m_validator; }
|
||||
const HttpValidator& GetValidator() const { return m_validator; }
|
||||
|
||||
// For testing: set mock mode (no actual network calls)
|
||||
void SetMockMode(bool enabled) { m_mock_mode = enabled; }
|
||||
bool IsMockMode() const { return m_mock_mode; }
|
||||
|
||||
private:
|
||||
std::string m_app_id;
|
||||
NetworkLimits m_limits;
|
||||
HttpValidator m_validator;
|
||||
std::atomic<int> m_active_requests{0};
|
||||
std::mutex m_mutex;
|
||||
bool m_mock_mode = true; // Default to mock mode for tests
|
||||
|
||||
// Validate request before sending
|
||||
bool ValidateRequest(const HttpRequest& request, std::string& error);
|
||||
};
|
||||
|
||||
// Register network.* APIs as globals
|
||||
void RegisterNetworkAPI(lua_State* L, NetworkManager* manager);
|
||||
|
||||
} // namespace mosis
|
||||
@@ -1,344 +0,0 @@
|
||||
#include "path_sandbox.h"
|
||||
|
||||
#include <lua.hpp>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace mosis {
|
||||
|
||||
//=============================================================================
|
||||
// CONSTRUCTOR
|
||||
//=============================================================================
|
||||
|
||||
PathSandbox::PathSandbox(const std::string& app_path)
|
||||
: m_app_path(app_path)
|
||||
{
|
||||
// Normalize the app path
|
||||
if (!m_app_path.empty()) {
|
||||
// Ensure trailing separator for prefix matching
|
||||
if (m_app_path.back() != '/' && m_app_path.back() != '\\') {
|
||||
m_app_path += '/';
|
||||
}
|
||||
// Normalize separators to forward slash
|
||||
std::replace(m_app_path.begin(), m_app_path.end(), '\\', '/');
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// PATH VALIDATION
|
||||
//=============================================================================
|
||||
|
||||
bool PathSandbox::ContainsTraversal(const std::string& path) {
|
||||
std::string normalized = NormalizePath(path);
|
||||
|
||||
// Check for .. anywhere in the path
|
||||
size_t pos = 0;
|
||||
while ((pos = normalized.find("..", pos)) != std::string::npos) {
|
||||
// Make sure it's actually a parent directory reference, not part of a filename
|
||||
bool at_start = (pos == 0);
|
||||
bool before_is_sep = (pos > 0 && (normalized[pos-1] == '/' || normalized[pos-1] == '\\'));
|
||||
|
||||
size_t after_pos = pos + 2;
|
||||
bool at_end = (after_pos >= normalized.size());
|
||||
bool after_is_sep = (!at_end && (normalized[after_pos] == '/' || normalized[after_pos] == '\\'));
|
||||
|
||||
if ((at_start || before_is_sep) && (at_end || after_is_sep)) {
|
||||
return true;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PathSandbox::IsAbsolutePath(const std::string& path) {
|
||||
if (path.empty()) return false;
|
||||
|
||||
// Unix absolute path
|
||||
if (path[0] == '/') return true;
|
||||
|
||||
// Windows absolute path (C:\ or C:/)
|
||||
if (path.length() >= 2) {
|
||||
char first = path[0];
|
||||
if (std::isalpha(static_cast<unsigned char>(first)) && path[1] == ':') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// UNC path (\\server\share or //server/share)
|
||||
if (path.length() >= 2) {
|
||||
if ((path[0] == '\\' && path[1] == '\\') ||
|
||||
(path[0] == '/' && path[1] == '/')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string PathSandbox::NormalizePath(const std::string& path) {
|
||||
std::string result = path;
|
||||
|
||||
// Convert backslashes to forward slashes
|
||||
std::replace(result.begin(), result.end(), '\\', '/');
|
||||
|
||||
// Remove leading ./
|
||||
while (result.length() >= 2 && result[0] == '.' && result[1] == '/') {
|
||||
result = result.substr(2);
|
||||
}
|
||||
|
||||
// Remove duplicate slashes
|
||||
std::string cleaned;
|
||||
bool last_was_slash = false;
|
||||
for (char c : result) {
|
||||
if (c == '/') {
|
||||
if (!last_was_slash) {
|
||||
cleaned += c;
|
||||
}
|
||||
last_was_slash = true;
|
||||
} else {
|
||||
cleaned += c;
|
||||
last_was_slash = false;
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
bool PathSandbox::ValidatePath(const std::string& path, std::string& out_canonical) {
|
||||
// Reject empty paths
|
||||
if (path.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject absolute paths
|
||||
if (IsAbsolutePath(path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject traversal attempts
|
||||
if (ContainsTraversal(path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Normalize and resolve the path
|
||||
std::string normalized = NormalizePath(path);
|
||||
std::string resolved = ResolvePath(normalized);
|
||||
|
||||
// Use filesystem to get canonical path (resolves any remaining .)
|
||||
try {
|
||||
std::filesystem::path fs_path(resolved);
|
||||
|
||||
// If the file exists, use canonical path for strict checking
|
||||
if (std::filesystem::exists(fs_path)) {
|
||||
std::filesystem::path canonical = std::filesystem::canonical(fs_path);
|
||||
std::string canonical_str = canonical.string();
|
||||
std::replace(canonical_str.begin(), canonical_str.end(), '\\', '/');
|
||||
|
||||
// Verify the canonical path is still within app_path
|
||||
std::string app_canonical = std::filesystem::canonical(
|
||||
std::filesystem::path(m_app_path)).string();
|
||||
std::replace(app_canonical.begin(), app_canonical.end(), '\\', '/');
|
||||
if (!app_canonical.empty() && app_canonical.back() != '/') {
|
||||
app_canonical += '/';
|
||||
}
|
||||
|
||||
if (canonical_str.rfind(app_canonical, 0) != 0) {
|
||||
return false; // Path escaped sandbox via symlink
|
||||
}
|
||||
|
||||
out_canonical = canonical_str;
|
||||
} else {
|
||||
// File doesn't exist, just use the resolved path
|
||||
out_canonical = resolved;
|
||||
}
|
||||
} catch (const std::filesystem::filesystem_error&) {
|
||||
// Filesystem error, use the resolved path as-is
|
||||
out_canonical = resolved;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string PathSandbox::ResolvePath(const std::string& relative_path) {
|
||||
std::string normalized = NormalizePath(relative_path);
|
||||
|
||||
// Combine with app path
|
||||
std::string result = m_app_path + normalized;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// MODULE NAME VALIDATION
|
||||
//=============================================================================
|
||||
|
||||
bool PathSandbox::IsValidModuleName(const std::string& name) {
|
||||
if (name.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check each character
|
||||
for (size_t i = 0; i < name.length(); i++) {
|
||||
char c = name[i];
|
||||
|
||||
// Allow alphanumeric
|
||||
if (std::isalnum(static_cast<unsigned char>(c))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Allow underscore
|
||||
if (c == '_') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Allow dot for submodules, but not at start/end or consecutive
|
||||
if (c == '.') {
|
||||
if (i == 0 || i == name.length() - 1) {
|
||||
return false; // Dot at start or end
|
||||
}
|
||||
if (i > 0 && name[i-1] == '.') {
|
||||
return false; // Consecutive dots
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Any other character is invalid
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject names that look like traversal
|
||||
if (name.find("..") != std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string PathSandbox::ModuleToPath(const std::string& module_name) {
|
||||
// Convert dots to path separators
|
||||
std::string path = module_name;
|
||||
std::replace(path.begin(), path.end(), '.', '/');
|
||||
|
||||
// Add scripts/ prefix and .lua suffix
|
||||
return "scripts/" + path + ".lua";
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// SAFE REQUIRE
|
||||
//=============================================================================
|
||||
|
||||
// Registry key for PathSandbox pointer
|
||||
static const char* SANDBOX_KEY = "mosis.path_sandbox";
|
||||
|
||||
// Registry key for loaded modules cache
|
||||
static const char* LOADED_KEY = "mosis.loaded_modules";
|
||||
|
||||
int SafeRequire(lua_State* L) {
|
||||
// Get module name
|
||||
const char* module_name = luaL_checkstring(L, 1);
|
||||
|
||||
// Validate module name
|
||||
if (!PathSandbox::IsValidModuleName(module_name)) {
|
||||
return luaL_error(L, "invalid module name: %s", module_name);
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_getfield(L, -1, module_name);
|
||||
if (!lua_isnil(L, -1)) {
|
||||
// Module already loaded, return cached value
|
||||
return 1;
|
||||
}
|
||||
lua_pop(L, 1); // Pop nil
|
||||
}
|
||||
lua_pop(L, 1); // Pop cache table (or nil if not exists)
|
||||
|
||||
// Get PathSandbox from registry
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, SANDBOX_KEY);
|
||||
if (!lua_islightuserdata(L, -1)) {
|
||||
return luaL_error(L, "require not properly initialized");
|
||||
}
|
||||
PathSandbox* sandbox = static_cast<PathSandbox*>(lua_touserdata(L, -1));
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Convert module name to path
|
||||
std::string relative_path = PathSandbox::ModuleToPath(module_name);
|
||||
|
||||
// Validate the path
|
||||
std::string canonical;
|
||||
if (!sandbox->ValidatePath(relative_path, canonical)) {
|
||||
return luaL_error(L, "cannot load module '%s': path validation failed", module_name);
|
||||
}
|
||||
|
||||
// Read the file
|
||||
std::ifstream file(canonical);
|
||||
if (!file.is_open()) {
|
||||
// Try with the resolved path directly (in case canonical check failed)
|
||||
std::string resolved = sandbox->ResolvePath(relative_path);
|
||||
file.open(resolved);
|
||||
if (!file.is_open()) {
|
||||
return luaL_error(L, "module '%s' not found", module_name);
|
||||
}
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
std::string source = buffer.str();
|
||||
file.close();
|
||||
|
||||
// Load as text only (no bytecode)
|
||||
std::string chunk_name = "@" + std::string(module_name);
|
||||
int status = luaL_loadbufferx(L, source.c_str(), source.size(),
|
||||
chunk_name.c_str(), "t");
|
||||
|
||||
if (status != LUA_OK) {
|
||||
return lua_error(L); // Propagate error
|
||||
}
|
||||
|
||||
// Execute the chunk
|
||||
lua_call(L, 0, 1);
|
||||
|
||||
// If chunk returned nil, use true as the module value
|
||||
if (lua_isnil(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
lua_pushboolean(L, 1);
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
|
||||
if (!lua_istable(L, -1)) {
|
||||
// Create cache table if it doesn't exist
|
||||
lua_pop(L, 1);
|
||||
lua_newtable(L);
|
||||
lua_pushvalue(L, -1);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
|
||||
}
|
||||
|
||||
// cache[module_name] = result
|
||||
lua_pushvalue(L, -2); // Push the result
|
||||
lua_setfield(L, -2, module_name);
|
||||
lua_pop(L, 1); // Pop cache table
|
||||
|
||||
// Return the module
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterSafeRequire(lua_State* L, PathSandbox* sandbox) {
|
||||
// Store PathSandbox pointer in registry
|
||||
lua_pushlightuserdata(L, sandbox);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, SANDBOX_KEY);
|
||||
|
||||
// Create loaded modules cache
|
||||
lua_newtable(L);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, LOADED_KEY);
|
||||
|
||||
// Register require function
|
||||
lua_pushcfunction(L, SafeRequire);
|
||||
lua_setglobal(L, "require");
|
||||
}
|
||||
|
||||
} // namespace mosis
|
||||
@@ -1,52 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
|
||||
struct lua_State;
|
||||
|
||||
namespace mosis {
|
||||
|
||||
class PathSandbox {
|
||||
public:
|
||||
explicit PathSandbox(const std::string& app_path);
|
||||
|
||||
// Validate a path is within the sandbox
|
||||
// Returns true if valid, sets out_canonical to the resolved path
|
||||
bool ValidatePath(const std::string& path, std::string& out_canonical);
|
||||
|
||||
// Check if path contains traversal attempts (..)
|
||||
static bool ContainsTraversal(const std::string& path);
|
||||
|
||||
// Check if path is absolute
|
||||
static bool IsAbsolutePath(const std::string& path);
|
||||
|
||||
// Normalize path separators and remove redundant ./ components
|
||||
static std::string NormalizePath(const std::string& path);
|
||||
|
||||
// Validate module name for require() - alphanumeric, underscore, dots only
|
||||
static bool IsValidModuleName(const std::string& name);
|
||||
|
||||
// Convert module name to relative path (e.g., "ui.button" -> "scripts/ui/button.lua")
|
||||
static std::string ModuleToPath(const std::string& module_name);
|
||||
|
||||
// Get the app's base path
|
||||
const std::string& GetAppPath() const { return m_app_path; }
|
||||
|
||||
// Resolve a relative path to full path within sandbox
|
||||
std::string ResolvePath(const std::string& relative_path);
|
||||
|
||||
private:
|
||||
std::string m_app_path;
|
||||
};
|
||||
|
||||
// Safe require implementation for Lua
|
||||
// Loads modules only from app_path/scripts/<module>.lua
|
||||
// Caches modules in registry
|
||||
int SafeRequire(lua_State* L);
|
||||
|
||||
// Register safe require as global "require"
|
||||
// The PathSandbox pointer is stored in registry for use by SafeRequire
|
||||
void RegisterSafeRequire(lua_State* L, PathSandbox* sandbox);
|
||||
|
||||
} // namespace mosis
|
||||
@@ -1,197 +0,0 @@
|
||||
#include "permission_gate.h"
|
||||
#include "lua_sandbox.h"
|
||||
|
||||
#include <lua.hpp>
|
||||
#include <algorithm>
|
||||
|
||||
namespace mosis {
|
||||
|
||||
//=============================================================================
|
||||
// PERMISSION DATABASE
|
||||
//=============================================================================
|
||||
|
||||
static const std::unordered_map<std::string, PermissionInfo> PERMISSIONS = {
|
||||
// Normal permissions (auto-granted when declared)
|
||||
{"internet", {PermissionCategory::Normal, "Access the internet"}},
|
||||
{"vibrate", {PermissionCategory::Normal, "Vibrate the device"}},
|
||||
{"wake_lock", {PermissionCategory::Normal, "Keep device awake"}},
|
||||
{"notifications", {PermissionCategory::Normal, "Show notifications"}},
|
||||
{"alarms", {PermissionCategory::Normal, "Set alarms"}},
|
||||
{"nfc", {PermissionCategory::Normal, "Use NFC"}},
|
||||
|
||||
// Dangerous permissions (require user consent)
|
||||
{"camera", {PermissionCategory::Dangerous, "Access the camera"}},
|
||||
{"microphone", {PermissionCategory::Dangerous, "Record audio"}},
|
||||
{"location.fine", {PermissionCategory::Dangerous, "Access precise location"}},
|
||||
{"location.coarse", {PermissionCategory::Dangerous, "Access approximate location"}},
|
||||
{"contacts.read", {PermissionCategory::Dangerous, "Read contacts"}},
|
||||
{"contacts.write", {PermissionCategory::Dangerous, "Modify contacts"}},
|
||||
{"storage.external", {PermissionCategory::Dangerous, "Access external storage"}},
|
||||
{"storage.shared", {PermissionCategory::Dangerous, "Access shared storage"}},
|
||||
{"sensors.motion", {PermissionCategory::Dangerous, "Access motion sensors"}},
|
||||
{"bluetooth", {PermissionCategory::Dangerous, "Use Bluetooth"}},
|
||||
{"bluetooth.scan", {PermissionCategory::Dangerous, "Scan for Bluetooth devices"}},
|
||||
{"calendar.read", {PermissionCategory::Dangerous, "Read calendar"}},
|
||||
{"calendar.write", {PermissionCategory::Dangerous, "Modify calendar"}},
|
||||
{"phone.call", {PermissionCategory::Dangerous, "Make phone calls"}},
|
||||
{"phone.read_state", {PermissionCategory::Dangerous, "Read phone state"}},
|
||||
{"sms.read", {PermissionCategory::Dangerous, "Read SMS messages"}},
|
||||
{"sms.send", {PermissionCategory::Dangerous, "Send SMS messages"}},
|
||||
|
||||
// Signature permissions (system apps only)
|
||||
{"system.settings", {PermissionCategory::Signature, "Modify system settings"}},
|
||||
{"system.install", {PermissionCategory::Signature, "Install apps"}},
|
||||
{"system.uninstall", {PermissionCategory::Signature, "Uninstall apps"}},
|
||||
{"system.admin", {PermissionCategory::Signature, "Device administrator"}},
|
||||
{"system.overlay", {PermissionCategory::Signature, "Display over other apps"}},
|
||||
{"system.wallpaper", {PermissionCategory::Signature, "Set wallpaper"}},
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// CONSTRUCTOR
|
||||
//=============================================================================
|
||||
|
||||
PermissionGate::PermissionGate(const SandboxContext& context)
|
||||
: m_context(context)
|
||||
, m_last_gesture(std::chrono::steady_clock::time_point::min())
|
||||
{
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// PERMISSION INFO
|
||||
//=============================================================================
|
||||
|
||||
PermissionCategory PermissionGate::GetCategory(const std::string& permission) {
|
||||
auto it = PERMISSIONS.find(permission);
|
||||
if (it != PERMISSIONS.end()) {
|
||||
return it->second.category;
|
||||
}
|
||||
// Unknown permissions default to Dangerous for safety
|
||||
return PermissionCategory::Dangerous;
|
||||
}
|
||||
|
||||
const PermissionInfo* PermissionGate::GetPermissionInfo(const std::string& permission) {
|
||||
auto it = PERMISSIONS.find(permission);
|
||||
if (it != PERMISSIONS.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// PERMISSION CHECKING
|
||||
//=============================================================================
|
||||
|
||||
bool PermissionGate::HasPermission(const std::string& permission) const {
|
||||
auto category = GetCategory(permission);
|
||||
|
||||
switch (category) {
|
||||
case PermissionCategory::Normal:
|
||||
return CheckNormalPermission(permission);
|
||||
case PermissionCategory::Dangerous:
|
||||
return CheckDangerousPermission(permission);
|
||||
case PermissionCategory::Signature:
|
||||
return CheckSignaturePermission(permission);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PermissionGate::Check(lua_State* L, const std::string& permission) {
|
||||
if (!HasPermission(permission)) {
|
||||
luaL_error(L, "permission denied: %s", permission.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PermissionGate::IsDeclared(const std::string& permission) const {
|
||||
const auto& declared = m_context.permissions;
|
||||
return std::find(declared.begin(), declared.end(), permission) != declared.end();
|
||||
}
|
||||
|
||||
bool PermissionGate::CheckNormalPermission(const std::string& permission) const {
|
||||
// Normal permissions are auto-granted if declared in manifest
|
||||
return IsDeclared(permission);
|
||||
}
|
||||
|
||||
bool PermissionGate::CheckDangerousPermission(const std::string& permission) const {
|
||||
// Must be declared in manifest
|
||||
if (!IsDeclared(permission)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// System apps get dangerous permissions automatically
|
||||
if (m_context.is_system_app) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Regular apps need runtime grant
|
||||
return m_runtime_grants.count(permission) > 0;
|
||||
}
|
||||
|
||||
bool PermissionGate::CheckSignaturePermission(const std::string& permission) const {
|
||||
// Only system apps get signature permissions
|
||||
if (!m_context.is_system_app) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must still be declared
|
||||
return IsDeclared(permission);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// USER GESTURE TRACKING
|
||||
//=============================================================================
|
||||
|
||||
void PermissionGate::RecordUserGesture() {
|
||||
m_last_gesture = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
bool PermissionGate::HasRecentUserGesture(int ms) const {
|
||||
// If no gesture has been recorded, return false
|
||||
if (m_last_gesture == std::chrono::steady_clock::time_point::min()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_last_gesture);
|
||||
return elapsed.count() < ms;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// RUNTIME GRANTS
|
||||
//=============================================================================
|
||||
|
||||
void PermissionGate::GrantPermission(const std::string& permission) {
|
||||
// Can only grant dangerous permissions
|
||||
auto category = GetCategory(permission);
|
||||
if (category == PermissionCategory::Dangerous) {
|
||||
m_runtime_grants.insert(permission);
|
||||
}
|
||||
}
|
||||
|
||||
void PermissionGate::RevokePermission(const std::string& permission) {
|
||||
m_runtime_grants.erase(permission);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// QUERIES
|
||||
//=============================================================================
|
||||
|
||||
const std::vector<std::string>& PermissionGate::GetDeclaredPermissions() const {
|
||||
return m_context.permissions;
|
||||
}
|
||||
|
||||
std::vector<std::string> PermissionGate::GetGrantedPermissions() const {
|
||||
std::vector<std::string> granted;
|
||||
|
||||
for (const auto& perm : m_context.permissions) {
|
||||
if (HasPermission(perm)) {
|
||||
granted.push_back(perm);
|
||||
}
|
||||
}
|
||||
|
||||
return granted;
|
||||
}
|
||||
|
||||
} // namespace mosis
|
||||
@@ -1,73 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
#include <chrono>
|
||||
|
||||
struct lua_State;
|
||||
|
||||
namespace mosis {
|
||||
|
||||
struct SandboxContext; // Forward declaration
|
||||
|
||||
enum class PermissionCategory {
|
||||
Normal, // Auto-granted when declared (e.g., internet, vibrate)
|
||||
Dangerous, // Requires user consent (e.g., camera, location)
|
||||
Signature // System apps only (e.g., system.settings)
|
||||
};
|
||||
|
||||
struct PermissionInfo {
|
||||
PermissionCategory category;
|
||||
std::string description;
|
||||
};
|
||||
|
||||
class PermissionGate {
|
||||
public:
|
||||
explicit PermissionGate(const SandboxContext& context);
|
||||
|
||||
// Check if app has permission (throws Lua error if not)
|
||||
bool Check(lua_State* L, const std::string& permission);
|
||||
|
||||
// Check without throwing (returns false if denied)
|
||||
bool HasPermission(const std::string& permission) const;
|
||||
|
||||
// Get permission category
|
||||
static PermissionCategory GetCategory(const std::string& permission);
|
||||
|
||||
// Get permission info (returns nullptr if unknown)
|
||||
static const PermissionInfo* GetPermissionInfo(const std::string& permission);
|
||||
|
||||
// User gesture tracking
|
||||
void RecordUserGesture();
|
||||
bool HasRecentUserGesture(int ms = 5000) const;
|
||||
|
||||
// Runtime permission grant (called after user consent)
|
||||
void GrantPermission(const std::string& permission);
|
||||
void RevokePermission(const std::string& permission);
|
||||
|
||||
// Get all declared permissions
|
||||
const std::vector<std::string>& GetDeclaredPermissions() const;
|
||||
|
||||
// Get all granted permissions
|
||||
std::vector<std::string> GetGrantedPermissions() const;
|
||||
|
||||
// Check if permission is declared in manifest
|
||||
bool IsDeclared(const std::string& permission) const;
|
||||
|
||||
private:
|
||||
const SandboxContext& m_context;
|
||||
std::unordered_set<std::string> m_runtime_grants; // Runtime-granted dangerous perms
|
||||
std::chrono::steady_clock::time_point m_last_gesture;
|
||||
|
||||
bool CheckNormalPermission(const std::string& permission) const;
|
||||
bool CheckDangerousPermission(const std::string& permission) const;
|
||||
bool CheckSignaturePermission(const std::string& permission) const;
|
||||
};
|
||||
|
||||
} // namespace mosis
|
||||
|
||||
// Convenience alias
|
||||
using PermissionGate = mosis::PermissionGate;
|
||||
using PermissionCategory = mosis::PermissionCategory;
|
||||
@@ -1,209 +0,0 @@
|
||||
#include "rate_limiter.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace mosis {
|
||||
|
||||
//=============================================================================
|
||||
// CONSTRUCTOR (with default limits)
|
||||
//=============================================================================
|
||||
|
||||
RateLimiter::RateLimiter() {
|
||||
// Network operations
|
||||
SetLimit("network.request", {10.0, 100.0}); // 10/sec, burst 100
|
||||
SetLimit("network.websocket", {2.0, 10.0}); // 2/sec, burst 10
|
||||
SetLimit("network.download", {5.0, 20.0}); // 5/sec, burst 20
|
||||
|
||||
// Storage operations
|
||||
SetLimit("storage.read", {100.0, 500.0}); // 100/sec, burst 500
|
||||
SetLimit("storage.write", {20.0, 100.0}); // 20/sec, burst 100
|
||||
SetLimit("storage.delete", {10.0, 50.0}); // 10/sec, burst 50
|
||||
SetLimit("database.query", {50.0, 200.0}); // 50/sec, burst 200
|
||||
|
||||
// Hardware access
|
||||
SetLimit("camera.capture", {30.0, 30.0}); // 30 fps max
|
||||
SetLimit("microphone.record", {1.0, 1.0}); // 1 session at a time
|
||||
SetLimit("location.request", {1.0, 5.0}); // 1/sec, burst 5
|
||||
SetLimit("sensor.read", {60.0, 60.0}); // 60 Hz max
|
||||
|
||||
// Timers
|
||||
SetLimit("timer.create", {10.0, 100.0}); // 10/sec, burst 100
|
||||
|
||||
// Crypto
|
||||
SetLimit("crypto.random", {100.0, 1000.0}); // 100/sec, burst 1000
|
||||
SetLimit("crypto.hash", {100.0, 1000.0}); // 100/sec, burst 1000
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// CONFIGURATION
|
||||
//=============================================================================
|
||||
|
||||
void RateLimiter::SetLimit(const std::string& operation, const RateLimitConfig& config) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_configs[operation] = config;
|
||||
}
|
||||
|
||||
const RateLimitConfig* RateLimiter::GetLimit(const std::string& operation) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
auto it = m_configs.find(operation);
|
||||
if (it != m_configs.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// CHECKING
|
||||
//=============================================================================
|
||||
|
||||
bool RateLimiter::Check(const std::string& app_id, const std::string& operation) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
// Find config
|
||||
auto config_it = m_configs.find(operation);
|
||||
if (config_it == m_configs.end()) {
|
||||
// No limit configured, allow by default
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto& config = config_it->second;
|
||||
auto& bucket = GetBucket(app_id, operation);
|
||||
|
||||
// Refill based on elapsed time
|
||||
Refill(bucket, config);
|
||||
|
||||
// Check if we have a token
|
||||
if (bucket.tokens >= 1.0) {
|
||||
bucket.tokens -= 1.0;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RateLimiter::CanProceed(const std::string& app_id, const std::string& operation) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
// Find config
|
||||
auto config_it = m_configs.find(operation);
|
||||
if (config_it == m_configs.end()) {
|
||||
return true; // No limit
|
||||
}
|
||||
|
||||
const auto& config = config_it->second;
|
||||
std::string key = MakeKey(app_id, operation);
|
||||
|
||||
auto bucket_it = m_buckets.find(key);
|
||||
if (bucket_it == m_buckets.end()) {
|
||||
return true; // New bucket would have full tokens
|
||||
}
|
||||
|
||||
// Make a copy to check without modifying
|
||||
Bucket bucket = bucket_it->second;
|
||||
Refill(bucket, config);
|
||||
|
||||
return bucket.tokens >= 1.0;
|
||||
}
|
||||
|
||||
double RateLimiter::GetTokens(const std::string& app_id, const std::string& operation) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
std::string key = MakeKey(app_id, operation);
|
||||
auto bucket_it = m_buckets.find(key);
|
||||
|
||||
if (bucket_it == m_buckets.end()) {
|
||||
// Check if there's a config
|
||||
auto config_it = m_configs.find(operation);
|
||||
if (config_it != m_configs.end()) {
|
||||
return config_it->second.max_tokens; // Would start with full
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Find config to refill
|
||||
auto config_it = m_configs.find(operation);
|
||||
if (config_it == m_configs.end()) {
|
||||
return bucket_it->second.tokens;
|
||||
}
|
||||
|
||||
// Make a copy to check without modifying
|
||||
Bucket bucket = bucket_it->second;
|
||||
Refill(bucket, config_it->second);
|
||||
|
||||
return bucket.tokens;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// RESET
|
||||
//=============================================================================
|
||||
|
||||
void RateLimiter::ResetApp(const std::string& app_id) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
// Find and remove all buckets for this app
|
||||
std::string prefix = app_id + ":";
|
||||
for (auto it = m_buckets.begin(); it != m_buckets.end(); ) {
|
||||
if (it->first.rfind(prefix, 0) == 0) { // starts with app_id:
|
||||
it = m_buckets.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RateLimiter::ClearAll() {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_buckets.clear();
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// INTERNAL
|
||||
//=============================================================================
|
||||
|
||||
void RateLimiter::Refill(Bucket& bucket, const RateLimitConfig& config) const {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto elapsed = std::chrono::duration<double>(now - bucket.last_refill);
|
||||
|
||||
// Add tokens based on elapsed time
|
||||
double new_tokens = bucket.tokens + (elapsed.count() * config.tokens_per_second);
|
||||
|
||||
// Cap at max
|
||||
bucket.tokens = std::min(new_tokens, config.max_tokens);
|
||||
bucket.last_refill = now;
|
||||
}
|
||||
|
||||
RateLimiter::Bucket& RateLimiter::GetBucket(const std::string& app_id,
|
||||
const std::string& operation) {
|
||||
std::string key = MakeKey(app_id, operation);
|
||||
|
||||
auto it = m_buckets.find(key);
|
||||
if (it != m_buckets.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Create new bucket with full tokens
|
||||
auto config_it = m_configs.find(operation);
|
||||
double initial = (config_it != m_configs.end()) ? config_it->second.max_tokens : 1.0;
|
||||
|
||||
m_buckets[key] = Bucket{
|
||||
.tokens = initial,
|
||||
.last_refill = std::chrono::steady_clock::now()
|
||||
};
|
||||
|
||||
return m_buckets[key];
|
||||
}
|
||||
|
||||
std::string RateLimiter::MakeKey(const std::string& app_id, const std::string& operation) {
|
||||
return app_id + ":" + operation;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// GLOBAL INSTANCE
|
||||
//=============================================================================
|
||||
|
||||
RateLimiter& GetRateLimiter() {
|
||||
static RateLimiter instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
} // namespace mosis
|
||||
@@ -1,68 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <chrono>
|
||||
|
||||
namespace mosis {
|
||||
|
||||
struct RateLimitConfig {
|
||||
double tokens_per_second; // Refill rate
|
||||
double max_tokens; // Bucket capacity
|
||||
};
|
||||
|
||||
class RateLimiter {
|
||||
public:
|
||||
// Default limits for common operations
|
||||
RateLimiter();
|
||||
|
||||
// Check if operation is allowed (consumes token if yes)
|
||||
bool Check(const std::string& app_id, const std::string& operation);
|
||||
|
||||
// Check without consuming token
|
||||
bool CanProceed(const std::string& app_id, const std::string& operation) const;
|
||||
|
||||
// Configure limits for an operation
|
||||
void SetLimit(const std::string& operation, const RateLimitConfig& config);
|
||||
|
||||
// Get config for an operation
|
||||
const RateLimitConfig* GetLimit(const std::string& operation) const;
|
||||
|
||||
// Get current token count for app+operation
|
||||
double GetTokens(const std::string& app_id, const std::string& operation) const;
|
||||
|
||||
// Reset all buckets for an app (e.g., on app restart)
|
||||
void ResetApp(const std::string& app_id);
|
||||
|
||||
// Clear all buckets
|
||||
void ClearAll();
|
||||
|
||||
private:
|
||||
struct Bucket {
|
||||
double tokens;
|
||||
std::chrono::steady_clock::time_point last_refill;
|
||||
};
|
||||
|
||||
// Refill bucket based on elapsed time
|
||||
void Refill(Bucket& bucket, const RateLimitConfig& config) const;
|
||||
|
||||
// Get or create bucket for app+operation
|
||||
Bucket& GetBucket(const std::string& app_id, const std::string& operation);
|
||||
|
||||
// Get bucket key
|
||||
static std::string MakeKey(const std::string& app_id, const std::string& operation);
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
std::unordered_map<std::string, RateLimitConfig> m_configs;
|
||||
mutable std::unordered_map<std::string, Bucket> m_buckets;
|
||||
};
|
||||
|
||||
// Global rate limiter (singleton)
|
||||
RateLimiter& GetRateLimiter();
|
||||
|
||||
} // namespace mosis
|
||||
|
||||
// Convenience alias
|
||||
using RateLimiter = mosis::RateLimiter;
|
||||
using RateLimitConfig = mosis::RateLimitConfig;
|
||||
@@ -2,14 +2,19 @@
|
||||
// Milestone 20: Kernel Integration
|
||||
#pragma once
|
||||
|
||||
#include "lua_sandbox.h"
|
||||
#include "permission_gate.h"
|
||||
#include "audit_log.h"
|
||||
#include "rate_limiter.h"
|
||||
#include "timer_manager.h"
|
||||
#include "virtual_fs.h"
|
||||
#include "database_manager.h"
|
||||
#include "network_manager.h"
|
||||
// Core library headers (shared across platforms)
|
||||
#include <mosis/sandbox/lua_sandbox.h>
|
||||
#include <mosis/sandbox/permission_gate.h>
|
||||
#include <mosis/sandbox/audit_log.h>
|
||||
#include <mosis/sandbox/rate_limiter.h>
|
||||
#include <mosis/sandbox/timer_manager.h>
|
||||
#include <mosis/sandbox/virtual_fs.h>
|
||||
#include <mosis/sandbox/database_manager.h>
|
||||
#include <mosis/sandbox/network_manager.h>
|
||||
#include <mosis/sandbox/json_api.h>
|
||||
#include <mosis/sandbox/crypto_api.h>
|
||||
|
||||
// Android-specific hardware interfaces
|
||||
#include "websocket_manager.h"
|
||||
#include "camera_interface.h"
|
||||
#include "microphone_interface.h"
|
||||
@@ -19,8 +24,6 @@
|
||||
#include "bluetooth_interface.h"
|
||||
#include "contacts_interface.h"
|
||||
#include "message_bus.h"
|
||||
#include "json_api.h"
|
||||
#include "crypto_api.h"
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -1,440 +0,0 @@
|
||||
#include "timer_manager.h"
|
||||
|
||||
#include <lua.hpp>
|
||||
#include <algorithm>
|
||||
|
||||
namespace mosis {
|
||||
|
||||
//=============================================================================
|
||||
// CONSTRUCTOR / DESTRUCTOR
|
||||
//=============================================================================
|
||||
|
||||
TimerManager::TimerManager() = default;
|
||||
|
||||
TimerManager::~TimerManager() {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
// Release all Lua callback references
|
||||
for (auto& timer : m_timers) {
|
||||
if (timer.callback_ref != LUA_NOREF && timer.L) {
|
||||
luaL_unref(timer.L, LUA_REGISTRYINDEX, timer.callback_ref);
|
||||
}
|
||||
}
|
||||
m_timers.clear();
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// TIMER CREATION
|
||||
//=============================================================================
|
||||
|
||||
TimerId TimerManager::SetTimeout(lua_State* L, const std::string& app_id,
|
||||
int callback_ref, int delay_ms) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
// Check per-app limit
|
||||
if (m_app_timer_counts[app_id] >= MAX_TIMERS_PER_APP) {
|
||||
// Release the callback reference since we're not using it
|
||||
luaL_unref(L, LUA_REGISTRYINDEX, callback_ref);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clamp delay
|
||||
if (delay_ms < MIN_TIMEOUT_MS) {
|
||||
delay_ms = MIN_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
Timer timer;
|
||||
timer.id = m_next_id++;
|
||||
timer.app_id = app_id;
|
||||
timer.fire_time = std::chrono::steady_clock::now() + Duration(delay_ms);
|
||||
timer.interval = Duration(0);
|
||||
timer.callback_ref = callback_ref;
|
||||
timer.L = L;
|
||||
timer.cancelled = false;
|
||||
timer.is_interval = false;
|
||||
|
||||
m_timers.push_back(timer);
|
||||
m_app_timer_counts[app_id]++;
|
||||
m_app_timer_ids[app_id].insert(timer.id);
|
||||
|
||||
return timer.id;
|
||||
}
|
||||
|
||||
TimerId TimerManager::SetInterval(lua_State* L, const std::string& app_id,
|
||||
int callback_ref, int interval_ms) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
// Check per-app limit
|
||||
if (m_app_timer_counts[app_id] >= MAX_TIMERS_PER_APP) {
|
||||
luaL_unref(L, LUA_REGISTRYINDEX, callback_ref);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clamp interval to minimum
|
||||
if (interval_ms < MIN_INTERVAL_MS) {
|
||||
interval_ms = MIN_INTERVAL_MS;
|
||||
}
|
||||
|
||||
Timer timer;
|
||||
timer.id = m_next_id++;
|
||||
timer.app_id = app_id;
|
||||
timer.fire_time = std::chrono::steady_clock::now() + Duration(interval_ms);
|
||||
timer.interval = Duration(interval_ms);
|
||||
timer.callback_ref = callback_ref;
|
||||
timer.L = L;
|
||||
timer.cancelled = false;
|
||||
timer.is_interval = true;
|
||||
|
||||
m_timers.push_back(timer);
|
||||
m_app_timer_counts[app_id]++;
|
||||
m_app_timer_ids[app_id].insert(timer.id);
|
||||
|
||||
return timer.id;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// TIMER CANCELLATION
|
||||
//=============================================================================
|
||||
|
||||
bool TimerManager::ClearTimer(const std::string& app_id, TimerId id) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
// Find the timer
|
||||
auto it = std::find_if(m_timers.begin(), m_timers.end(),
|
||||
[id, &app_id](const Timer& t) {
|
||||
return t.id == id && t.app_id == app_id && !t.cancelled;
|
||||
});
|
||||
|
||||
if (it == m_timers.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mark as cancelled (will be removed during ProcessTimers)
|
||||
it->cancelled = true;
|
||||
|
||||
// Release the Lua callback reference
|
||||
if (it->callback_ref != LUA_NOREF && it->L) {
|
||||
luaL_unref(it->L, LUA_REGISTRYINDEX, it->callback_ref);
|
||||
it->callback_ref = LUA_NOREF;
|
||||
}
|
||||
|
||||
// Update counts
|
||||
if (m_app_timer_counts[app_id] > 0) {
|
||||
m_app_timer_counts[app_id]--;
|
||||
}
|
||||
m_app_timer_ids[app_id].erase(id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void TimerManager::ClearAppTimers(const std::string& app_id) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
// Get all timer IDs for this app
|
||||
auto it = m_app_timer_ids.find(app_id);
|
||||
if (it == m_app_timer_ids.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark all timers as cancelled and release references
|
||||
for (auto& timer : m_timers) {
|
||||
if (timer.app_id == app_id && !timer.cancelled) {
|
||||
timer.cancelled = true;
|
||||
if (timer.callback_ref != LUA_NOREF && timer.L) {
|
||||
luaL_unref(timer.L, LUA_REGISTRYINDEX, timer.callback_ref);
|
||||
timer.callback_ref = LUA_NOREF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear tracking
|
||||
m_app_timer_counts[app_id] = 0;
|
||||
m_app_timer_ids[app_id].clear();
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// TIMER PROCESSING
|
||||
//=============================================================================
|
||||
|
||||
void TimerManager::FireTimer(Timer& timer) {
|
||||
if (timer.cancelled || timer.callback_ref == LUA_NOREF || !timer.L) {
|
||||
return;
|
||||
}
|
||||
|
||||
lua_State* L = timer.L;
|
||||
|
||||
// Get the callback from registry
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, timer.callback_ref);
|
||||
|
||||
if (lua_isfunction(L, -1)) {
|
||||
// Call the callback with protected call
|
||||
int result = lua_pcall(L, 0, 0, 0);
|
||||
if (result != LUA_OK) {
|
||||
// Log error but don't propagate
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
} else {
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
|
||||
void TimerManager::RescheduleInterval(Timer& timer) {
|
||||
// Update fire time for next interval
|
||||
timer.fire_time = std::chrono::steady_clock::now() + timer.interval;
|
||||
}
|
||||
|
||||
int TimerManager::ProcessTimers() {
|
||||
// We need to be careful here - firing a timer might cause
|
||||
// new timers to be added or timers to be cancelled
|
||||
|
||||
std::vector<Timer> to_fire;
|
||||
std::vector<TimerId> to_reschedule;
|
||||
std::vector<TimerId> to_remove;
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
// Find all timers that should fire
|
||||
for (auto& timer : m_timers) {
|
||||
if (timer.cancelled) {
|
||||
to_remove.push_back(timer.id);
|
||||
} else if (timer.fire_time <= now) {
|
||||
to_fire.push_back(timer);
|
||||
if (timer.is_interval) {
|
||||
to_reschedule.push_back(timer.id);
|
||||
} else {
|
||||
to_remove.push_back(timer.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fire timers outside the lock to allow callbacks to create new timers
|
||||
int fired_count = 0;
|
||||
for (auto& timer : to_fire) {
|
||||
FireTimer(timer);
|
||||
fired_count++;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
// Reschedule intervals
|
||||
for (TimerId id : to_reschedule) {
|
||||
auto it = std::find_if(m_timers.begin(), m_timers.end(),
|
||||
[id](const Timer& t) { return t.id == id && !t.cancelled; });
|
||||
if (it != m_timers.end()) {
|
||||
RescheduleInterval(*it);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove completed/cancelled timers
|
||||
for (TimerId id : to_remove) {
|
||||
auto it = std::find_if(m_timers.begin(), m_timers.end(),
|
||||
[id](const Timer& t) { return t.id == id; });
|
||||
if (it != m_timers.end()) {
|
||||
// Release reference if not already released
|
||||
if (it->callback_ref != LUA_NOREF && it->L && !it->is_interval) {
|
||||
luaL_unref(it->L, LUA_REGISTRYINDEX, it->callback_ref);
|
||||
}
|
||||
|
||||
// Update counts only for non-cancelled (timeout) timers
|
||||
if (!it->cancelled && !it->is_interval) {
|
||||
const std::string& app_id = it->app_id;
|
||||
if (m_app_timer_counts[app_id] > 0) {
|
||||
m_app_timer_counts[app_id]--;
|
||||
}
|
||||
m_app_timer_ids[app_id].erase(id);
|
||||
}
|
||||
|
||||
m_timers.erase(it);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fired_count;
|
||||
}
|
||||
|
||||
size_t TimerManager::GetTimerCount(const std::string& app_id) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
auto it = m_app_timer_counts.find(app_id);
|
||||
if (it == m_app_timer_counts.end()) {
|
||||
return 0;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// LUA API
|
||||
//=============================================================================
|
||||
|
||||
// Registry keys for storing manager pointer and app_id
|
||||
static const char* TIMER_MANAGER_KEY = "__mosis_timer_manager";
|
||||
static const char* TIMER_APP_ID_KEY = "__mosis_timer_app_id";
|
||||
|
||||
// setTimeout(callback, delay_ms) -> timer_id
|
||||
static int lua_setTimeout(lua_State* L) {
|
||||
// Check arguments
|
||||
luaL_checktype(L, 1, LUA_TFUNCTION);
|
||||
int delay_ms = static_cast<int>(luaL_checkinteger(L, 2));
|
||||
|
||||
// Get timer manager from registry
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, TIMER_MANAGER_KEY);
|
||||
if (!lua_islightuserdata(L, -1)) {
|
||||
return luaL_error(L, "timer system not initialized");
|
||||
}
|
||||
TimerManager* manager = static_cast<TimerManager*>(lua_touserdata(L, -1));
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Get app_id from registry
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, TIMER_APP_ID_KEY);
|
||||
if (!lua_isstring(L, -1)) {
|
||||
return luaL_error(L, "app_id not set");
|
||||
}
|
||||
std::string app_id = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Store the callback in registry
|
||||
lua_pushvalue(L, 1); // Push the callback
|
||||
int callback_ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||
|
||||
// Create the timer
|
||||
TimerId id = manager->SetTimeout(L, app_id, callback_ref, delay_ms);
|
||||
|
||||
if (id == 0) {
|
||||
return luaL_error(L, "timer limit exceeded");
|
||||
}
|
||||
|
||||
lua_pushinteger(L, static_cast<lua_Integer>(id));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// clearTimeout(timer_id)
|
||||
static int lua_clearTimeout(lua_State* L) {
|
||||
TimerId id = static_cast<TimerId>(luaL_checkinteger(L, 1));
|
||||
|
||||
// Get timer manager from registry
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, TIMER_MANAGER_KEY);
|
||||
if (!lua_islightuserdata(L, -1)) {
|
||||
return luaL_error(L, "timer system not initialized");
|
||||
}
|
||||
TimerManager* manager = static_cast<TimerManager*>(lua_touserdata(L, -1));
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Get app_id from registry
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, TIMER_APP_ID_KEY);
|
||||
if (!lua_isstring(L, -1)) {
|
||||
return luaL_error(L, "app_id not set");
|
||||
}
|
||||
std::string app_id = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
manager->ClearTimer(app_id, id);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// setInterval(callback, interval_ms) -> timer_id
|
||||
static int lua_setInterval(lua_State* L) {
|
||||
// Check arguments
|
||||
luaL_checktype(L, 1, LUA_TFUNCTION);
|
||||
int interval_ms = static_cast<int>(luaL_checkinteger(L, 2));
|
||||
|
||||
// Get timer manager from registry
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, TIMER_MANAGER_KEY);
|
||||
if (!lua_islightuserdata(L, -1)) {
|
||||
return luaL_error(L, "timer system not initialized");
|
||||
}
|
||||
TimerManager* manager = static_cast<TimerManager*>(lua_touserdata(L, -1));
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Get app_id from registry
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, TIMER_APP_ID_KEY);
|
||||
if (!lua_isstring(L, -1)) {
|
||||
return luaL_error(L, "app_id not set");
|
||||
}
|
||||
std::string app_id = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Store the callback in registry
|
||||
lua_pushvalue(L, 1); // Push the callback
|
||||
int callback_ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||
|
||||
// Create the timer
|
||||
TimerId id = manager->SetInterval(L, app_id, callback_ref, interval_ms);
|
||||
|
||||
if (id == 0) {
|
||||
return luaL_error(L, "timer limit exceeded");
|
||||
}
|
||||
|
||||
lua_pushinteger(L, static_cast<lua_Integer>(id));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// clearInterval(timer_id)
|
||||
static int lua_clearInterval(lua_State* L) {
|
||||
// Same as clearTimeout
|
||||
return lua_clearTimeout(L);
|
||||
}
|
||||
|
||||
void RegisterTimerAPI(lua_State* L, TimerManager* manager, const std::string& app_id) {
|
||||
// Store timer manager pointer in registry
|
||||
lua_pushlightuserdata(L, manager);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, TIMER_MANAGER_KEY);
|
||||
|
||||
// Store app_id in registry
|
||||
lua_pushstring(L, app_id.c_str());
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, TIMER_APP_ID_KEY);
|
||||
|
||||
// Get the real _G (not the proxy)
|
||||
// We need to set these in the real global table that the proxy reads from
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
|
||||
|
||||
// Check if we're dealing with a proxy (has __index metatable)
|
||||
if (lua_getmetatable(L, -1)) {
|
||||
lua_getfield(L, -1, "__index");
|
||||
if (lua_istable(L, -1)) {
|
||||
// We have a proxy, use the __index table as the real _G
|
||||
lua_remove(L, -2); // Remove metatable
|
||||
lua_remove(L, -2); // Remove proxy
|
||||
|
||||
// Now top of stack is real _G
|
||||
lua_pushcfunction(L, lua_setTimeout);
|
||||
lua_setfield(L, -2, "setTimeout");
|
||||
|
||||
lua_pushcfunction(L, lua_clearTimeout);
|
||||
lua_setfield(L, -2, "clearTimeout");
|
||||
|
||||
lua_pushcfunction(L, lua_setInterval);
|
||||
lua_setfield(L, -2, "setInterval");
|
||||
|
||||
lua_pushcfunction(L, lua_clearInterval);
|
||||
lua_setfield(L, -2, "clearInterval");
|
||||
|
||||
lua_pop(L, 1); // Pop real _G
|
||||
return;
|
||||
}
|
||||
lua_pop(L, 2); // Pop __index and metatable
|
||||
}
|
||||
|
||||
// No proxy, just use _G directly
|
||||
lua_pop(L, 1); // Pop whatever we got from LUA_RIDX_GLOBALS
|
||||
|
||||
// Register as globals
|
||||
lua_pushcfunction(L, lua_setTimeout);
|
||||
lua_setglobal(L, "setTimeout");
|
||||
|
||||
lua_pushcfunction(L, lua_clearTimeout);
|
||||
lua_setglobal(L, "clearTimeout");
|
||||
|
||||
lua_pushcfunction(L, lua_setInterval);
|
||||
lua_setglobal(L, "setInterval");
|
||||
|
||||
lua_pushcfunction(L, lua_clearInterval);
|
||||
lua_setglobal(L, "clearInterval");
|
||||
}
|
||||
|
||||
} // namespace mosis
|
||||
@@ -1,87 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <cstdint>
|
||||
|
||||
struct lua_State;
|
||||
|
||||
namespace mosis {
|
||||
|
||||
using TimerId = uint64_t;
|
||||
using TimePoint = std::chrono::steady_clock::time_point;
|
||||
using Duration = std::chrono::milliseconds;
|
||||
|
||||
struct Timer {
|
||||
TimerId id;
|
||||
std::string app_id;
|
||||
TimePoint fire_time;
|
||||
Duration interval; // 0 for setTimeout, >0 for setInterval
|
||||
int callback_ref; // Lua registry reference
|
||||
lua_State* L; // Lua state that owns the callback
|
||||
bool cancelled = false;
|
||||
bool is_interval = false;
|
||||
};
|
||||
|
||||
class TimerManager {
|
||||
public:
|
||||
TimerManager();
|
||||
~TimerManager();
|
||||
|
||||
// Non-copyable
|
||||
TimerManager(const TimerManager&) = delete;
|
||||
TimerManager& operator=(const TimerManager&) = delete;
|
||||
|
||||
// Create timers (returns timer ID, 0 on failure)
|
||||
TimerId SetTimeout(lua_State* L, const std::string& app_id,
|
||||
int callback_ref, int delay_ms);
|
||||
TimerId SetInterval(lua_State* L, const std::string& app_id,
|
||||
int callback_ref, int interval_ms);
|
||||
|
||||
// Cancel timers
|
||||
bool ClearTimer(const std::string& app_id, TimerId id);
|
||||
|
||||
// Cancel all timers for an app (call on app stop)
|
||||
void ClearAppTimers(const std::string& app_id);
|
||||
|
||||
// Process timers (call from main loop)
|
||||
// Returns number of timers fired
|
||||
int ProcessTimers();
|
||||
|
||||
// Get timer count for an app
|
||||
size_t GetTimerCount(const std::string& app_id) const;
|
||||
|
||||
// Configuration
|
||||
static constexpr size_t MAX_TIMERS_PER_APP = 100;
|
||||
static constexpr int MIN_INTERVAL_MS = 10;
|
||||
static constexpr int MIN_TIMEOUT_MS = 0;
|
||||
|
||||
private:
|
||||
TimerId m_next_id = 1;
|
||||
|
||||
// All timers (we use a vector and sort/search as needed)
|
||||
std::vector<Timer> m_timers;
|
||||
|
||||
// Track timer count per app
|
||||
std::unordered_map<std::string, size_t> m_app_timer_counts;
|
||||
|
||||
// Track which timer IDs belong to which app (for fast cancellation)
|
||||
std::unordered_map<std::string, std::unordered_set<TimerId>> m_app_timer_ids;
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
|
||||
void FireTimer(Timer& timer);
|
||||
void RemoveTimer(TimerId id);
|
||||
void RescheduleInterval(Timer& timer);
|
||||
};
|
||||
|
||||
// Lua API registration
|
||||
// Registers: setTimeout, clearTimeout, setInterval, clearInterval
|
||||
void RegisterTimerAPI(lua_State* L, TimerManager* manager, const std::string& app_id);
|
||||
|
||||
} // namespace mosis
|
||||
@@ -1,706 +0,0 @@
|
||||
#include "virtual_fs.h"
|
||||
|
||||
#include <lua.hpp>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace mosis {
|
||||
|
||||
//=============================================================================
|
||||
// VIRTUALFS IMPLEMENTATION
|
||||
//=============================================================================
|
||||
|
||||
VirtualFS::VirtualFS(const std::string& app_id,
|
||||
const std::string& app_root,
|
||||
const VirtualFSLimits& limits)
|
||||
: m_app_id(app_id)
|
||||
, m_app_root(app_root)
|
||||
, m_limits(limits) {
|
||||
// Ensure app root exists
|
||||
std::error_code ec;
|
||||
fs::create_directories(m_app_root, ec);
|
||||
|
||||
// Recalculate usage on startup
|
||||
RecalculateUsage();
|
||||
}
|
||||
|
||||
VirtualFS::~VirtualFS() {
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// PATH VALIDATION
|
||||
//=============================================================================
|
||||
|
||||
bool VirtualFS::IsValidPathChar(char c) {
|
||||
// Allow alphanumeric, dash, underscore, dot, forward slash
|
||||
return std::isalnum(static_cast<unsigned char>(c)) ||
|
||||
c == '-' || c == '_' || c == '.' || c == '/';
|
||||
}
|
||||
|
||||
int VirtualFS::GetPathDepth(const std::string& path) {
|
||||
int depth = 0;
|
||||
for (char c : path) {
|
||||
if (c == '/') depth++;
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
|
||||
bool VirtualFS::ValidatePath(const std::string& virtual_path, std::string& error) {
|
||||
// Check length
|
||||
if (virtual_path.length() > m_limits.max_path_length) {
|
||||
error = "path too long";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must start with /
|
||||
if (virtual_path.empty() || virtual_path[0] != '/') {
|
||||
error = "path must start with /";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check valid prefix
|
||||
bool valid_prefix = false;
|
||||
if (virtual_path.find("/data/") == 0 || virtual_path == "/data") {
|
||||
valid_prefix = true;
|
||||
} else if (virtual_path.find("/cache/") == 0 || virtual_path == "/cache") {
|
||||
valid_prefix = true;
|
||||
} else if (virtual_path.find("/temp/") == 0 || virtual_path == "/temp") {
|
||||
valid_prefix = true;
|
||||
} else if (virtual_path.find("/shared/") == 0 || virtual_path == "/shared") {
|
||||
// Check permission for shared
|
||||
if (CheckPermission && !CheckPermission("storage.shared")) {
|
||||
error = "permission denied: storage.shared required";
|
||||
return false;
|
||||
}
|
||||
valid_prefix = true;
|
||||
}
|
||||
|
||||
if (!valid_prefix) {
|
||||
error = "invalid path prefix (must be /data/, /cache/, /temp/, or /shared/)";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for path traversal
|
||||
if (virtual_path.find("..") != std::string::npos) {
|
||||
error = "path traversal not allowed";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for double slashes (except at start)
|
||||
if (virtual_path.find("//") != std::string::npos) {
|
||||
error = "invalid path (double slashes)";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check all characters are valid
|
||||
for (char c : virtual_path) {
|
||||
if (!IsValidPathChar(c)) {
|
||||
error = "invalid character in path";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check depth
|
||||
if (GetPathDepth(virtual_path) > m_limits.max_path_depth) {
|
||||
error = "path too deep";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string VirtualFS::ResolvePath(const std::string& virtual_path) {
|
||||
// Map virtual path to physical path
|
||||
// /data/foo.txt -> <app_root>/data/foo.txt
|
||||
// /cache/bar.txt -> <app_root>/cache/bar.txt
|
||||
// /temp/baz.txt -> <app_root>/temp/baz.txt
|
||||
// /shared/x.txt -> <app_root>/shared/x.txt
|
||||
|
||||
fs::path base(m_app_root);
|
||||
|
||||
// Remove leading slash and append
|
||||
std::string relative = virtual_path.substr(1); // Remove leading /
|
||||
|
||||
return (base / relative).string();
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// FILE OPERATIONS
|
||||
//=============================================================================
|
||||
|
||||
bool VirtualFS::EnsureParentDir(const std::string& path) {
|
||||
fs::path p(path);
|
||||
fs::path parent = p.parent_path();
|
||||
|
||||
if (parent.empty()) return true;
|
||||
|
||||
std::error_code ec;
|
||||
fs::create_directories(parent, ec);
|
||||
return !ec;
|
||||
}
|
||||
|
||||
void VirtualFS::UpdateUsage(int64_t delta) {
|
||||
if (delta < 0 && static_cast<size_t>(-delta) > m_used_bytes) {
|
||||
m_used_bytes = 0;
|
||||
} else {
|
||||
m_used_bytes = static_cast<size_t>(static_cast<int64_t>(m_used_bytes) + delta);
|
||||
}
|
||||
}
|
||||
|
||||
bool VirtualFS::CheckQuota(size_t additional_bytes, std::string& error) {
|
||||
if (m_used_bytes + additional_bytes > m_limits.max_quota_bytes) {
|
||||
error = "quota exceeded";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<std::string> VirtualFS::Read(const std::string& path, std::string& error) {
|
||||
if (!ValidatePath(path, error)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string physical_path = ResolvePath(path);
|
||||
|
||||
std::ifstream file(physical_path, std::ios::binary);
|
||||
if (!file) {
|
||||
error = "file not found";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::ostringstream ss;
|
||||
ss << file.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
bool VirtualFS::Write(const std::string& path, const std::string& data, std::string& error) {
|
||||
if (!ValidatePath(path, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check file size limit
|
||||
if (data.size() > m_limits.max_file_size) {
|
||||
error = "file size limit exceeded";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string physical_path = ResolvePath(path);
|
||||
|
||||
// Get current file size if exists (for quota calculation)
|
||||
size_t old_size = 0;
|
||||
std::error_code ec;
|
||||
if (fs::exists(physical_path, ec)) {
|
||||
old_size = static_cast<size_t>(fs::file_size(physical_path, ec));
|
||||
}
|
||||
|
||||
// Check quota for net change
|
||||
int64_t delta = static_cast<int64_t>(data.size()) - static_cast<int64_t>(old_size);
|
||||
if (delta > 0 && !CheckQuota(static_cast<size_t>(delta), error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure parent directory exists
|
||||
if (!EnsureParentDir(physical_path)) {
|
||||
error = "failed to create parent directory";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream file(physical_path, std::ios::binary | std::ios::trunc);
|
||||
if (!file) {
|
||||
error = "failed to open file for writing";
|
||||
return false;
|
||||
}
|
||||
|
||||
file.write(data.data(), data.size());
|
||||
if (!file) {
|
||||
error = "failed to write data";
|
||||
return false;
|
||||
}
|
||||
|
||||
file.close();
|
||||
UpdateUsage(delta);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VirtualFS::Append(const std::string& path, const std::string& data, std::string& error) {
|
||||
if (!ValidatePath(path, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string physical_path = ResolvePath(path);
|
||||
|
||||
// Get current file size
|
||||
size_t current_size = 0;
|
||||
std::error_code ec;
|
||||
if (fs::exists(physical_path, ec)) {
|
||||
current_size = static_cast<size_t>(fs::file_size(physical_path, ec));
|
||||
}
|
||||
|
||||
// Check file size limit
|
||||
if (current_size + data.size() > m_limits.max_file_size) {
|
||||
error = "file size limit exceeded";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check quota
|
||||
if (!CheckQuota(data.size(), error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure parent directory exists
|
||||
if (!EnsureParentDir(physical_path)) {
|
||||
error = "failed to create parent directory";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream file(physical_path, std::ios::binary | std::ios::app);
|
||||
if (!file) {
|
||||
error = "failed to open file for appending";
|
||||
return false;
|
||||
}
|
||||
|
||||
file.write(data.data(), data.size());
|
||||
if (!file) {
|
||||
error = "failed to append data";
|
||||
return false;
|
||||
}
|
||||
|
||||
file.close();
|
||||
UpdateUsage(static_cast<int64_t>(data.size()));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VirtualFS::Delete(const std::string& path, std::string& error) {
|
||||
if (!ValidatePath(path, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string physical_path = ResolvePath(path);
|
||||
|
||||
std::error_code ec;
|
||||
if (!fs::exists(physical_path, ec)) {
|
||||
error = "file not found";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get size before deletion
|
||||
size_t file_size = 0;
|
||||
if (fs::is_regular_file(physical_path, ec)) {
|
||||
file_size = static_cast<size_t>(fs::file_size(physical_path, ec));
|
||||
}
|
||||
|
||||
if (!fs::remove(physical_path, ec)) {
|
||||
error = "failed to delete";
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateUsage(-static_cast<int64_t>(file_size));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VirtualFS::Exists(const std::string& path) {
|
||||
std::string error;
|
||||
if (!ValidatePath(path, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string physical_path = ResolvePath(path);
|
||||
std::error_code ec;
|
||||
return fs::exists(physical_path, ec);
|
||||
}
|
||||
|
||||
std::optional<std::vector<std::string>> VirtualFS::List(const std::string& path, std::string& error) {
|
||||
if (!ValidatePath(path, error)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string physical_path = ResolvePath(path);
|
||||
|
||||
std::error_code ec;
|
||||
if (!fs::exists(physical_path, ec) || !fs::is_directory(physical_path, ec)) {
|
||||
error = "directory not found";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<std::string> entries;
|
||||
for (const auto& entry : fs::directory_iterator(physical_path, ec)) {
|
||||
entries.push_back(entry.path().filename().string());
|
||||
}
|
||||
|
||||
if (ec) {
|
||||
error = "failed to list directory";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
bool VirtualFS::MakeDir(const std::string& path, std::string& error) {
|
||||
if (!ValidatePath(path, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string physical_path = ResolvePath(path);
|
||||
|
||||
std::error_code ec;
|
||||
if (!fs::create_directories(physical_path, ec) && ec) {
|
||||
error = "failed to create directory";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<FileStat> VirtualFS::Stat(const std::string& path, std::string& error) {
|
||||
if (!ValidatePath(path, error)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string physical_path = ResolvePath(path);
|
||||
|
||||
std::error_code ec;
|
||||
if (!fs::exists(physical_path, ec)) {
|
||||
error = "file not found";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
FileStat stat;
|
||||
stat.is_dir = fs::is_directory(physical_path, ec);
|
||||
|
||||
if (stat.is_dir) {
|
||||
stat.size = 0;
|
||||
} else {
|
||||
stat.size = static_cast<size_t>(fs::file_size(physical_path, ec));
|
||||
}
|
||||
|
||||
auto ftime = fs::last_write_time(physical_path, ec);
|
||||
// Convert file_time_type to system_clock (portable workaround for clock_cast)
|
||||
auto file_time_ns = ftime.time_since_epoch();
|
||||
auto sys_time_ns = std::chrono::duration_cast<std::chrono::seconds>(file_time_ns);
|
||||
stat.modified = sys_time_ns.count();
|
||||
|
||||
return stat;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// CLEANUP
|
||||
//=============================================================================
|
||||
|
||||
void VirtualFS::DeleteDirectoryRecursive(const std::string& path) {
|
||||
std::error_code ec;
|
||||
fs::remove_all(path, ec);
|
||||
}
|
||||
|
||||
size_t VirtualFS::CalculateDirectorySize(const std::string& path) {
|
||||
size_t total = 0;
|
||||
std::error_code ec;
|
||||
|
||||
if (!fs::exists(path, ec)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (const auto& entry : fs::recursive_directory_iterator(path, ec)) {
|
||||
if (fs::is_regular_file(entry, ec)) {
|
||||
total += static_cast<size_t>(fs::file_size(entry, ec));
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
void VirtualFS::RecalculateUsage() {
|
||||
m_used_bytes = CalculateDirectorySize(m_app_root);
|
||||
}
|
||||
|
||||
void VirtualFS::ClearTemp() {
|
||||
fs::path temp_path = fs::path(m_app_root) / "temp";
|
||||
|
||||
std::error_code ec;
|
||||
if (fs::exists(temp_path, ec)) {
|
||||
size_t temp_size = CalculateDirectorySize(temp_path.string());
|
||||
DeleteDirectoryRecursive(temp_path.string());
|
||||
UpdateUsage(-static_cast<int64_t>(temp_size));
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualFS::ClearAll() {
|
||||
DeleteDirectoryRecursive(m_app_root);
|
||||
m_used_bytes = 0;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// LUA API
|
||||
//=============================================================================
|
||||
|
||||
static const char* VFS_KEY = "__mosis_vfs";
|
||||
|
||||
static VirtualFS* GetVFS(lua_State* L) {
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, VFS_KEY);
|
||||
if (lua_islightuserdata(L, -1)) {
|
||||
VirtualFS* vfs = static_cast<VirtualFS*>(lua_touserdata(L, -1));
|
||||
lua_pop(L, 1);
|
||||
return vfs;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// fs.read(path) -> content|nil, error
|
||||
static int lua_fs_read(lua_State* L) {
|
||||
VirtualFS* vfs = GetVFS(L);
|
||||
if (!vfs) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, "VirtualFS not initialized");
|
||||
return 2;
|
||||
}
|
||||
|
||||
const char* path = luaL_checkstring(L, 1);
|
||||
std::string error;
|
||||
|
||||
auto content = vfs->Read(path, error);
|
||||
if (content) {
|
||||
lua_pushlstring(L, content->data(), content->size());
|
||||
return 1;
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, error.c_str());
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
// fs.write(path, data) -> bool, error
|
||||
static int lua_fs_write(lua_State* L) {
|
||||
VirtualFS* vfs = GetVFS(L);
|
||||
if (!vfs) {
|
||||
lua_pushboolean(L, 0);
|
||||
lua_pushstring(L, "VirtualFS not initialized");
|
||||
return 2;
|
||||
}
|
||||
|
||||
const char* path = luaL_checkstring(L, 1);
|
||||
size_t len;
|
||||
const char* data = luaL_checklstring(L, 2, &len);
|
||||
std::string error;
|
||||
|
||||
if (vfs->Write(path, std::string(data, len), error)) {
|
||||
lua_pushboolean(L, 1);
|
||||
return 1;
|
||||
} else {
|
||||
lua_pushboolean(L, 0);
|
||||
lua_pushstring(L, error.c_str());
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
// fs.append(path, data) -> bool, error
|
||||
static int lua_fs_append(lua_State* L) {
|
||||
VirtualFS* vfs = GetVFS(L);
|
||||
if (!vfs) {
|
||||
lua_pushboolean(L, 0);
|
||||
lua_pushstring(L, "VirtualFS not initialized");
|
||||
return 2;
|
||||
}
|
||||
|
||||
const char* path = luaL_checkstring(L, 1);
|
||||
size_t len;
|
||||
const char* data = luaL_checklstring(L, 2, &len);
|
||||
std::string error;
|
||||
|
||||
if (vfs->Append(path, std::string(data, len), error)) {
|
||||
lua_pushboolean(L, 1);
|
||||
return 1;
|
||||
} else {
|
||||
lua_pushboolean(L, 0);
|
||||
lua_pushstring(L, error.c_str());
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
// fs.delete(path) -> bool, error
|
||||
static int lua_fs_delete(lua_State* L) {
|
||||
VirtualFS* vfs = GetVFS(L);
|
||||
if (!vfs) {
|
||||
lua_pushboolean(L, 0);
|
||||
lua_pushstring(L, "VirtualFS not initialized");
|
||||
return 2;
|
||||
}
|
||||
|
||||
const char* path = luaL_checkstring(L, 1);
|
||||
std::string error;
|
||||
|
||||
if (vfs->Delete(path, error)) {
|
||||
lua_pushboolean(L, 1);
|
||||
return 1;
|
||||
} else {
|
||||
lua_pushboolean(L, 0);
|
||||
lua_pushstring(L, error.c_str());
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
// fs.exists(path) -> bool
|
||||
static int lua_fs_exists(lua_State* L) {
|
||||
VirtualFS* vfs = GetVFS(L);
|
||||
if (!vfs) {
|
||||
lua_pushboolean(L, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* path = luaL_checkstring(L, 1);
|
||||
lua_pushboolean(L, vfs->Exists(path) ? 1 : 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// fs.list(path) -> array|nil, error
|
||||
static int lua_fs_list(lua_State* L) {
|
||||
VirtualFS* vfs = GetVFS(L);
|
||||
if (!vfs) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, "VirtualFS not initialized");
|
||||
return 2;
|
||||
}
|
||||
|
||||
const char* path = luaL_checkstring(L, 1);
|
||||
std::string error;
|
||||
|
||||
auto entries = vfs->List(path, error);
|
||||
if (entries) {
|
||||
lua_createtable(L, static_cast<int>(entries->size()), 0);
|
||||
int i = 1;
|
||||
for (const auto& name : *entries) {
|
||||
lua_pushlstring(L, name.c_str(), name.size());
|
||||
lua_rawseti(L, -2, i++);
|
||||
}
|
||||
return 1;
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, error.c_str());
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
// fs.mkdir(path) -> bool, error
|
||||
static int lua_fs_mkdir(lua_State* L) {
|
||||
VirtualFS* vfs = GetVFS(L);
|
||||
if (!vfs) {
|
||||
lua_pushboolean(L, 0);
|
||||
lua_pushstring(L, "VirtualFS not initialized");
|
||||
return 2;
|
||||
}
|
||||
|
||||
const char* path = luaL_checkstring(L, 1);
|
||||
std::string error;
|
||||
|
||||
if (vfs->MakeDir(path, error)) {
|
||||
lua_pushboolean(L, 1);
|
||||
return 1;
|
||||
} else {
|
||||
lua_pushboolean(L, 0);
|
||||
lua_pushstring(L, error.c_str());
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
// fs.stat(path) -> {size, modified, isDir}|nil, error
|
||||
static int lua_fs_stat(lua_State* L) {
|
||||
VirtualFS* vfs = GetVFS(L);
|
||||
if (!vfs) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, "VirtualFS not initialized");
|
||||
return 2;
|
||||
}
|
||||
|
||||
const char* path = luaL_checkstring(L, 1);
|
||||
std::string error;
|
||||
|
||||
auto stat = vfs->Stat(path, error);
|
||||
if (stat) {
|
||||
lua_createtable(L, 0, 3);
|
||||
|
||||
lua_pushinteger(L, static_cast<lua_Integer>(stat->size));
|
||||
lua_setfield(L, -2, "size");
|
||||
|
||||
lua_pushinteger(L, static_cast<lua_Integer>(stat->modified));
|
||||
lua_setfield(L, -2, "modified");
|
||||
|
||||
lua_pushboolean(L, stat->is_dir ? 1 : 0);
|
||||
lua_setfield(L, -2, "isDir");
|
||||
|
||||
return 1;
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, error.c_str());
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to set a global in the real _G (bypassing any proxy)
|
||||
static void SetGlobalInRealG(lua_State* L, const char* name) {
|
||||
// Stack: value to set as global
|
||||
|
||||
// Get _G (might be a proxy)
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
|
||||
|
||||
// Check if it has a metatable with __index (proxy pattern)
|
||||
if (lua_getmetatable(L, -1)) {
|
||||
lua_getfield(L, -1, "__index");
|
||||
if (lua_istable(L, -1)) {
|
||||
// Found real _G through proxy's __index
|
||||
// Stack: value, proxy, mt, real_G
|
||||
lua_pushvalue(L, -4); // Copy value
|
||||
lua_setfield(L, -2, name); // real_G[name] = value
|
||||
lua_pop(L, 4); // pop real_G, mt, proxy, original value
|
||||
return;
|
||||
}
|
||||
lua_pop(L, 2); // pop __index, metatable
|
||||
}
|
||||
|
||||
// No proxy, set directly in _G
|
||||
// Stack: value, _G
|
||||
lua_pushvalue(L, -2); // Copy value
|
||||
lua_setfield(L, -2, name); // _G[name] = value
|
||||
lua_pop(L, 2); // pop _G, original value
|
||||
}
|
||||
|
||||
void RegisterVirtualFS(lua_State* L, VirtualFS* vfs) {
|
||||
// Store VFS in registry
|
||||
lua_pushlightuserdata(L, vfs);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, VFS_KEY);
|
||||
|
||||
// Create fs table
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushcfunction(L, lua_fs_read);
|
||||
lua_setfield(L, -2, "read");
|
||||
|
||||
lua_pushcfunction(L, lua_fs_write);
|
||||
lua_setfield(L, -2, "write");
|
||||
|
||||
lua_pushcfunction(L, lua_fs_append);
|
||||
lua_setfield(L, -2, "append");
|
||||
|
||||
lua_pushcfunction(L, lua_fs_delete);
|
||||
lua_setfield(L, -2, "delete");
|
||||
|
||||
lua_pushcfunction(L, lua_fs_exists);
|
||||
lua_setfield(L, -2, "exists");
|
||||
|
||||
lua_pushcfunction(L, lua_fs_list);
|
||||
lua_setfield(L, -2, "list");
|
||||
|
||||
lua_pushcfunction(L, lua_fs_mkdir);
|
||||
lua_setfield(L, -2, "mkdir");
|
||||
|
||||
lua_pushcfunction(L, lua_fs_stat);
|
||||
lua_setfield(L, -2, "stat");
|
||||
|
||||
// Set as global (bypassing proxy)
|
||||
SetGlobalInRealG(L, "fs");
|
||||
}
|
||||
|
||||
} // namespace mosis
|
||||
@@ -1,77 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <functional>
|
||||
|
||||
struct lua_State;
|
||||
|
||||
namespace mosis {
|
||||
|
||||
struct FileStat {
|
||||
size_t size;
|
||||
int64_t modified; // Unix timestamp
|
||||
bool is_dir;
|
||||
};
|
||||
|
||||
struct VirtualFSLimits {
|
||||
size_t max_quota_bytes = 50 * 1024 * 1024; // 50 MB per app
|
||||
size_t max_file_size = 10 * 1024 * 1024; // 10 MB per file
|
||||
int max_path_depth = 10; // Max directory depth
|
||||
size_t max_path_length = 256; // Max path string length
|
||||
};
|
||||
|
||||
class VirtualFS {
|
||||
public:
|
||||
VirtualFS(const std::string& app_id,
|
||||
const std::string& app_root,
|
||||
const VirtualFSLimits& limits = VirtualFSLimits{});
|
||||
~VirtualFS();
|
||||
|
||||
// Path operations
|
||||
bool ValidatePath(const std::string& virtual_path, std::string& error);
|
||||
std::string ResolvePath(const std::string& virtual_path);
|
||||
|
||||
// File operations
|
||||
std::optional<std::string> Read(const std::string& path, std::string& error);
|
||||
bool Write(const std::string& path, const std::string& data, std::string& error);
|
||||
bool Append(const std::string& path, const std::string& data, std::string& error);
|
||||
bool Delete(const std::string& path, std::string& error);
|
||||
bool Exists(const std::string& path);
|
||||
std::optional<std::vector<std::string>> List(const std::string& path, std::string& error);
|
||||
bool MakeDir(const std::string& path, std::string& error);
|
||||
std::optional<FileStat> Stat(const std::string& path, std::string& error);
|
||||
|
||||
// Quota management
|
||||
size_t GetUsedBytes() const { return m_used_bytes; }
|
||||
size_t GetQuotaBytes() const { return m_limits.max_quota_bytes; }
|
||||
void RecalculateUsage();
|
||||
|
||||
// Cleanup
|
||||
void ClearTemp();
|
||||
void ClearAll(); // For testing
|
||||
|
||||
// Permission check callback (set by sandbox)
|
||||
std::function<bool(const std::string&)> CheckPermission;
|
||||
|
||||
private:
|
||||
std::string m_app_id;
|
||||
std::string m_app_root;
|
||||
VirtualFSLimits m_limits;
|
||||
size_t m_used_bytes = 0;
|
||||
|
||||
bool EnsureParentDir(const std::string& path);
|
||||
void UpdateUsage(int64_t delta);
|
||||
bool CheckQuota(size_t additional_bytes, std::string& error);
|
||||
int GetPathDepth(const std::string& path);
|
||||
bool IsValidPathChar(char c);
|
||||
void DeleteDirectoryRecursive(const std::string& path);
|
||||
size_t CalculateDirectorySize(const std::string& path);
|
||||
};
|
||||
|
||||
// Register fs.* APIs as globals
|
||||
void RegisterVirtualFS(lua_State* L, VirtualFS* vfs);
|
||||
|
||||
} // namespace mosis
|
||||
@@ -6,7 +6,7 @@
|
||||
#include <mutex>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
#include "http_validator.h"
|
||||
#include <mosis/sandbox/http_validator.h>
|
||||
|
||||
struct lua_State;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <vector>
|
||||
#include <format>
|
||||
#include "assets_manager.h"
|
||||
#include "logger.h"
|
||||
#include <mosis/util/logger.h>
|
||||
|
||||
void Shader::destroy()
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"freetype",
|
||||
"nlohmann-json",
|
||||
"minizip",
|
||||
"sqlite3"
|
||||
"sqlite3",
|
||||
"openssl"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user