diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt new file mode 100644 index 0000000..fea5845 --- /dev/null +++ b/core/CMakeLists.txt @@ -0,0 +1,100 @@ +cmake_minimum_required(VERSION 3.18) +project(mosis-core VERSION 1.0.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Core library sources - portable sandbox APIs +set(MOSIS_CORE_SOURCES + # Utility + src/logger.cpp + + # Sandbox APIs (portable) + src/sandbox/timer_manager.cpp + src/sandbox/json_api.cpp + src/sandbox/crypto_api.cpp + src/sandbox/virtual_fs.cpp + src/sandbox/lua_sandbox.cpp + src/sandbox/permission_gate.cpp + src/sandbox/audit_log.cpp + src/sandbox/rate_limiter.cpp + src/sandbox/path_sandbox.cpp +) + +# Optional sources that require additional dependencies +if(MOSIS_ENABLE_DATABASE) + list(APPEND MOSIS_CORE_SOURCES src/sandbox/database_manager.cpp) +endif() + +if(MOSIS_ENABLE_NETWORK) + list(APPEND MOSIS_CORE_SOURCES + src/sandbox/network_manager.cpp + src/sandbox/http_validator.cpp + ) +endif() + +# Create static library +add_library(mosis-core STATIC ${MOSIS_CORE_SOURCES}) + +# Include directories +target_include_directories(mosis-core PUBLIC + $ + $ + $ +) + +# Also add internal include path for relative includes within library +target_include_directories(mosis-core PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include/mosis/sandbox + ${CMAKE_CURRENT_SOURCE_DIR}/include/mosis/util + ${CMAKE_CURRENT_SOURCE_DIR}/include/mosis/apps +) + +# Platform-specific definitions +if(ANDROID) + target_compile_definitions(mosis-core PUBLIC MOSIS_PLATFORM_ANDROID) +elseif(WIN32) + target_compile_definitions(mosis-core PUBLIC MOSIS_PLATFORM_WINDOWS) + # Windows crypto library + target_link_libraries(mosis-core PRIVATE bcrypt) +elseif(APPLE) + target_compile_definitions(mosis-core PUBLIC MOSIS_PLATFORM_MACOS) +else() + target_compile_definitions(mosis-core PUBLIC MOSIS_PLATFORM_LINUX) +endif() + +# Lua is required - parent project must provide it +if(TARGET lua_static) + target_link_libraries(mosis-core PUBLIC lua_static) +elseif(TARGET lua) + target_link_libraries(mosis-core PUBLIC lua) +else() + # Try to find Lua + find_package(Lua QUIET) + if(LUA_FOUND) + target_include_directories(mosis-core PUBLIC ${LUA_INCLUDE_DIR}) + target_link_libraries(mosis-core PUBLIC ${LUA_LIBRARIES}) + else() + message(WARNING "Lua not found - parent project must provide Lua target") + endif() +endif() + +# JSON library (nlohmann/json - header only) +if(TARGET nlohmann_json::nlohmann_json) + target_link_libraries(mosis-core PUBLIC nlohmann_json::nlohmann_json) +else() + find_package(nlohmann_json QUIET) + if(nlohmann_json_FOUND) + target_link_libraries(mosis-core PUBLIC nlohmann_json::nlohmann_json) + endif() +endif() + +# SQLite for database_manager (optional) +if(MOSIS_ENABLE_DATABASE) + find_package(SQLite3 REQUIRED) + target_link_libraries(mosis-core PRIVATE SQLite::SQLite3) + target_compile_definitions(mosis-core PRIVATE MOSIS_HAS_SQLITE) +endif() + +# Export compile commands for IDE support +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) diff --git a/core/include/mosis/apps/app_api.h b/core/include/mosis/apps/app_api.h new file mode 100644 index 0000000..ec80482 --- /dev/null +++ b/core/include/mosis/apps/app_api.h @@ -0,0 +1,23 @@ +// app_api.h - Lua API bindings for app management +// Milestone 10: Device-Side App Management +#pragma once + +#include + +struct lua_State; + +namespace mosis { + +class AppManager; +class UpdateService; + +// Register Lua APIs for app management +// - mosis.apps.* - System apps only (App Store, Settings) +// - mosis.app.* - All apps (info about current app) +void RegisterAppAPIs(lua_State* L, + AppManager* app_manager, + UpdateService* update_service, + const std::string& current_app_id, + bool is_system_app); + +} // namespace mosis diff --git a/core/include/mosis/apps/app_manager.h b/core/include/mosis/apps/app_manager.h new file mode 100644 index 0000000..c2a452c --- /dev/null +++ b/core/include/mosis/apps/app_manager.h @@ -0,0 +1,167 @@ +// app_manager.h - App installation and management +// Milestone 10: Device-Side App Management +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mosis { + +// Forward declarations +class LuaSandboxManager; + +// Information about an installed app +struct InstalledApp { + std::string package_id; + std::string name; + std::string version_name; + int version_code = 0; + std::string install_path; + std::vector permissions; + std::chrono::system_clock::time_point installed_at; + std::chrono::system_clock::time_point updated_at; + int64_t package_size = 0; + int64_t data_size = 0; + bool is_system_app = false; + std::string entry_point; + std::string icon_path; + std::string developer_name; +}; + +// Progress stages during installation +struct InstallProgress { + enum class Stage { + Downloading, + Verifying, + Extracting, + Registering, + Complete, + Failed + }; + + Stage stage = Stage::Downloading; + float progress = 0.0f; // 0.0 - 1.0 + std::string error; + + static const char* StageName(Stage s) { + switch (s) { + case Stage::Downloading: return "downloading"; + case Stage::Verifying: return "verifying"; + case Stage::Extracting: return "extracting"; + case Stage::Registering: return "registering"; + case Stage::Complete: return "complete"; + case Stage::Failed: return "failed"; + default: return "unknown"; + } + } +}; + +using ProgressCallback = std::function; + +// Manifest parsed from package +struct AppManifest { + std::string id; + std::string name; + std::string version; + int version_code = 0; + std::string entry; + std::string icon; + std::string description; + std::string developer_name; + std::string developer_email; + std::vector permissions; + int min_api_version = 1; +}; + +class AppManager { +public: + explicit AppManager(const std::string& data_root); + ~AppManager(); + + // Prevent copying + AppManager(const AppManager&) = delete; + AppManager& operator=(const AppManager&) = delete; + + // Installation from URL + bool Install(const std::string& package_url, + const std::string& signature, + ProgressCallback callback); + + // Installation from local file + bool InstallFromFile(const std::string& package_path, + ProgressCallback callback); + + // Uninstallation + bool Uninstall(const std::string& package_id, bool keep_data = false); + + // Updates + bool Update(const std::string& package_id, + const std::string& package_url, + const std::string& signature, + ProgressCallback callback); + + // Query installed apps + std::vector GetInstalledApps() const; + std::optional GetApp(const std::string& package_id) const; + bool IsInstalled(const std::string& package_id) const; + + // Data management + int64_t GetAppDataSize(const std::string& package_id) const; + bool ClearAppData(const std::string& package_id); + bool ClearAppCache(const std::string& package_id); + bool BackupAppData(const std::string& package_id); + bool RestoreAppData(const std::string& package_id); + + // App launching + bool LaunchApp(const std::string& package_id); + bool StopApp(const std::string& package_id); + bool IsAppRunning(const std::string& package_id) const; + + // Integration with sandbox manager + void SetSandboxManager(LuaSandboxManager* manager); + + // Get paths + std::string GetDataRoot() const { return m_data_root; } + std::string GetAppPath(const std::string& package_id) const; + std::string GetAppDataPath(const std::string& package_id) const; + std::string GetAppCachePath(const std::string& package_id) const; + + // System apps registration + void RegisterSystemApp(const InstalledApp& app); + +private: + // Package verification + bool VerifyPackage(const std::string& path); + bool VerifySignature(const std::string& path, const std::string& signature); + + // Package operations + std::optional ExtractManifest(const std::string& package_path); + bool ExtractPackage(const std::string& package_path, const std::string& dest_path); + + // Download helper + bool DownloadFile(const std::string& url, const std::string& dest_path, + std::function progress_callback); + + // Registry persistence + void LoadInstalledApps(); + void SaveInstalledApps(); + + // Directory size calculation + int64_t CalculateDirectorySize(const std::string& path) const; + + // Generate unique ID + std::string GenerateUUID() const; + + std::string m_data_root; + LuaSandboxManager* m_sandbox_manager = nullptr; + mutable std::mutex m_mutex; + std::map m_installed_apps; +}; + +} // namespace mosis diff --git a/core/include/mosis/platform/asset_interface.h b/core/include/mosis/platform/asset_interface.h new file mode 100644 index 0000000..77fa582 --- /dev/null +++ b/core/include/mosis/platform/asset_interface.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include + +namespace mosis { + +/** + * Platform-agnostic interface for loading assets. + * Android implements this using AAssetManager. + * Desktop implements this using filesystem operations. + */ +class IAssetInterface { +public: + virtual ~IAssetInterface() = default; + + /** + * Read entire file contents as bytes. + * @param path Relative path to asset (e.g., "apps/home/home.rml") + * @return File contents, or empty vector if not found + */ + virtual std::vector ReadFile(const std::string& path) = 0; + + /** + * Read entire file contents as string. + * @param path Relative path to asset + * @return File contents, or empty string if not found + */ + virtual std::string ReadFileString(const std::string& path) = 0; + + /** + * Check if an asset exists. + * @param path Relative path to asset + * @return true if asset exists + */ + virtual bool Exists(const std::string& path) = 0; + + /** + * List files in a directory. + * @param path Relative path to directory + * @return List of filenames (not full paths) + */ + virtual std::vector ListDirectory(const std::string& path) = 0; + + /** + * Get the absolute path for an asset (if applicable). + * On Android this may return empty as assets are in APK. + * @param path Relative path to asset + * @return Absolute path or empty string + */ + virtual std::string GetAbsolutePath(const std::string& path) = 0; +}; + +using AssetInterfacePtr = std::shared_ptr; + +} // namespace mosis diff --git a/core/include/mosis/platform/filesystem_interface.h b/core/include/mosis/platform/filesystem_interface.h new file mode 100644 index 0000000..790c8b9 --- /dev/null +++ b/core/include/mosis/platform/filesystem_interface.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include +#include + +namespace mosis { + +/** + * Platform-agnostic interface for filesystem operations. + * Used for app data storage, not assets. + */ +class IFilesystemInterface { +public: + virtual ~IFilesystemInterface() = default; + + /** + * Get the base data directory for apps. + * Android: /data/data/com.omixlab.mosis/files/ + * Desktop: ./data/ or configurable + */ + virtual std::string GetDataRoot() = 0; + + /** + * Get the apps installation directory. + * Contains installed app packages. + */ + virtual std::string GetAppsDirectory() = 0; + + /** + * Get app-specific data directory. + * @param app_id Application ID (e.g., "com.example.app") + */ + virtual std::string GetAppDataDirectory(const std::string& app_id) = 0; + + /** + * Get app-specific cache directory. + */ + virtual std::string GetAppCacheDirectory(const std::string& app_id) = 0; + + /** + * Create directory if it doesn't exist. + * @return true on success + */ + virtual bool CreateDirectory(const std::string& path) = 0; + + /** + * Check if path exists. + */ + virtual bool Exists(const std::string& path) = 0; + + /** + * Check if path is a directory. + */ + virtual bool IsDirectory(const std::string& path) = 0; + + /** + * Remove file or directory. + * @param recursive If true, remove directory contents + */ + virtual bool Remove(const std::string& path, bool recursive = false) = 0; + + /** + * Read file contents. + */ + virtual std::vector ReadFile(const std::string& path) = 0; + + /** + * Write file contents. + */ + virtual bool WriteFile(const std::string& path, const std::vector& data) = 0; + + /** + * List directory contents. + */ + virtual std::vector ListDirectory(const std::string& path) = 0; + + /** + * Get file size. + * @return Size in bytes, or -1 if not found + */ + virtual int64_t GetFileSize(const std::string& path) = 0; + + /** + * Copy file. + */ + virtual bool CopyFile(const std::string& src, const std::string& dst) = 0; + + /** + * Move/rename file. + */ + virtual bool MoveFile(const std::string& src, const std::string& dst) = 0; +}; + +using FilesystemInterfacePtr = std::shared_ptr; + +} // namespace mosis diff --git a/core/include/mosis/sandbox/audit_log.h b/core/include/mosis/sandbox/audit_log.h new file mode 100644 index 0000000..c42333a --- /dev/null +++ b/core/include/mosis/sandbox/audit_log.h @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include + +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 GetEntries(size_t count = 100) const; + std::vector GetEntriesForApp(const std::string& app_id, + size_t count = 100) const; + std::vector 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 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; diff --git a/core/include/mosis/sandbox/crypto_api.h b/core/include/mosis/sandbox/crypto_api.h new file mode 100644 index 0000000..a0fee8f --- /dev/null +++ b/core/include/mosis/sandbox/crypto_api.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include + +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 diff --git a/core/include/mosis/sandbox/database_manager.h b/core/include/mosis/sandbox/database_manager.h new file mode 100644 index 0000000..11a972a --- /dev/null +++ b/core/include/mosis/sandbox/database_manager.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +struct sqlite3; +struct lua_State; + +namespace mosis { + +// SQL value types +using SqlValue = std::variant>; +using SqlRow = std::vector; +using SqlResult = std::vector; + +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 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> 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& params, std::string& error); + + // Query (SELECT) + std::optional Query(const std::string& sql, const std::vector& 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& params, std::string& error); +}; + +// Register database.* APIs as globals +void RegisterDatabaseAPI(lua_State* L, DatabaseManager* manager); + +} // namespace mosis diff --git a/core/include/mosis/sandbox/http_validator.h b/core/include/mosis/sandbox/http_validator.h new file mode 100644 index 0000000..79b3334 --- /dev/null +++ b/core/include/mosis/sandbox/http_validator.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include + +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& domains); + + // Clear domain restrictions (for testing) + void ClearDomainRestrictions(); + + // Validate URL + // Returns parsed URL on success, sets error on failure + std::optional Validate(const std::string& url, std::string& error); + +private: + std::vector 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 ParseUrl(const std::string& url); +}; + +} // namespace mosis diff --git a/core/include/mosis/sandbox/json_api.h b/core/include/mosis/sandbox/json_api.h new file mode 100644 index 0000000..5f59d94 --- /dev/null +++ b/core/include/mosis/sandbox/json_api.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +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 diff --git a/core/include/mosis/sandbox/lua_sandbox.h b/core/include/mosis/sandbox/lua_sandbox.h new file mode 100644 index 0000000..3f16da1 --- /dev/null +++ b/core/include/mosis/sandbox/lua_sandbox.h @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include + +// 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 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; diff --git a/core/include/mosis/sandbox/network_manager.h b/core/include/mosis/sandbox/network_manager.h new file mode 100644 index 0000000..1f547db --- /dev/null +++ b/core/include/mosis/sandbox/network_manager.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "http_validator.h" + +struct lua_State; + +namespace mosis { + +struct HttpRequest { + std::string url; + std::string method = "GET"; + std::map headers; + std::string body; + int timeout_ms = 30000; +}; + +struct HttpResponse { + int status_code = 0; + std::map 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& 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 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 diff --git a/core/include/mosis/sandbox/path_sandbox.h b/core/include/mosis/sandbox/path_sandbox.h new file mode 100644 index 0000000..3eb7d59 --- /dev/null +++ b/core/include/mosis/sandbox/path_sandbox.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +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/.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 diff --git a/core/include/mosis/sandbox/permission_gate.h b/core/include/mosis/sandbox/permission_gate.h new file mode 100644 index 0000000..0160353 --- /dev/null +++ b/core/include/mosis/sandbox/permission_gate.h @@ -0,0 +1,73 @@ +#pragma once + +#include +#include +#include +#include +#include + +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& GetDeclaredPermissions() const; + + // Get all granted permissions + std::vector GetGrantedPermissions() const; + + // Check if permission is declared in manifest + bool IsDeclared(const std::string& permission) const; + +private: + const SandboxContext& m_context; + std::unordered_set 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; diff --git a/core/include/mosis/sandbox/rate_limiter.h b/core/include/mosis/sandbox/rate_limiter.h new file mode 100644 index 0000000..a068e0b --- /dev/null +++ b/core/include/mosis/sandbox/rate_limiter.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include +#include + +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 m_configs; + mutable std::unordered_map m_buckets; +}; + +// Global rate limiter (singleton) +RateLimiter& GetRateLimiter(); + +} // namespace mosis + +// Convenience alias +using RateLimiter = mosis::RateLimiter; +using RateLimitConfig = mosis::RateLimitConfig; diff --git a/core/include/mosis/sandbox/timer_manager.h b/core/include/mosis/sandbox/timer_manager.h new file mode 100644 index 0000000..99585f5 --- /dev/null +++ b/core/include/mosis/sandbox/timer_manager.h @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +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 m_timers; + + // Track timer count per app + std::unordered_map m_app_timer_counts; + + // Track which timer IDs belong to which app (for fast cancellation) + std::unordered_map> 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 diff --git a/core/include/mosis/sandbox/virtual_fs.h b/core/include/mosis/sandbox/virtual_fs.h new file mode 100644 index 0000000..21e1044 --- /dev/null +++ b/core/include/mosis/sandbox/virtual_fs.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include +#include +#include + +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 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> List(const std::string& path, std::string& error); + bool MakeDir(const std::string& path, std::string& error); + std::optional 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 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 diff --git a/core/include/mosis/util/logger.h b/core/include/mosis/util/logger.h new file mode 100644 index 0000000..458744a --- /dev/null +++ b/core/include/mosis/util/logger.h @@ -0,0 +1,40 @@ +#pragma once +#include +#include +#include + +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__) diff --git a/core/src/apps/app_api.cpp b/core/src/apps/app_api.cpp new file mode 100644 index 0000000..0527656 --- /dev/null +++ b/core/src/apps/app_api.cpp @@ -0,0 +1,539 @@ +// app_api.cpp - Lua API bindings for app management implementation +// Milestone 10: Device-Side App Management + +#include "app_api.h" +#include "app_manager.h" +#include "update_service.h" +#include "../logger.h" + +#include +#include +#include + +namespace mosis { + +// Registry keys for storing pointers +static const char* APP_MANAGER_KEY = "mosis.app_manager"; +static const char* UPDATE_SERVICE_KEY = "mosis.update_service"; +static const char* CURRENT_APP_ID_KEY = "mosis.current_app_id"; +static const char* IS_SYSTEM_APP_KEY = "mosis.is_system_app"; + +// Helper to get AppManager from Lua registry +static AppManager* GetAppManager(lua_State* L) { + lua_getfield(L, LUA_REGISTRYINDEX, APP_MANAGER_KEY); + auto* manager = static_cast(lua_touserdata(L, -1)); + lua_pop(L, 1); + return manager; +} + +// Helper to get UpdateService from Lua registry +static UpdateService* GetUpdateService(lua_State* L) { + lua_getfield(L, LUA_REGISTRYINDEX, UPDATE_SERVICE_KEY); + auto* service = static_cast(lua_touserdata(L, -1)); + lua_pop(L, 1); + return service; +} + +// Helper to get current app ID +static std::string GetCurrentAppId(lua_State* L) { + lua_getfield(L, LUA_REGISTRYINDEX, CURRENT_APP_ID_KEY); + std::string id = lua_tostring(L, -1) ? lua_tostring(L, -1) : ""; + lua_pop(L, 1); + return id; +} + +// Helper to check if system app +static bool IsSystemApp(lua_State* L) { + lua_getfield(L, LUA_REGISTRYINDEX, IS_SYSTEM_APP_KEY); + bool is_system = lua_toboolean(L, -1); + lua_pop(L, 1); + return is_system; +} + +// ============================================================================ +// mosis.apps.* - System apps only +// ============================================================================ + +// mosis.apps.getInstalled() -> [{package_id, name, version_name, version_code, installed_at}] +static int apps_getInstalled(lua_State* L) { + if (!IsSystemApp(L)) { + return luaL_error(L, "mosis.apps.getInstalled requires system permission"); + } + + auto* manager = GetAppManager(L); + if (!manager) { + lua_newtable(L); + return 1; + } + + auto apps = manager->GetInstalledApps(); + + lua_createtable(L, static_cast(apps.size()), 0); + int idx = 1; + + for (const auto& app : apps) { + lua_createtable(L, 0, 8); + + lua_pushstring(L, app.package_id.c_str()); + lua_setfield(L, -2, "package_id"); + + lua_pushstring(L, app.name.c_str()); + lua_setfield(L, -2, "name"); + + lua_pushstring(L, app.version_name.c_str()); + lua_setfield(L, -2, "version_name"); + + lua_pushinteger(L, app.version_code); + lua_setfield(L, -2, "version_code"); + + lua_pushboolean(L, app.is_system_app); + lua_setfield(L, -2, "is_system_app"); + + lua_pushstring(L, app.icon_path.c_str()); + lua_setfield(L, -2, "icon"); + + lua_pushstring(L, app.developer_name.c_str()); + lua_setfield(L, -2, "developer"); + + // installed_at as Unix timestamp + auto ts = std::chrono::duration_cast( + app.installed_at.time_since_epoch()).count(); + lua_pushinteger(L, ts); + lua_setfield(L, -2, "installed_at"); + + lua_rawseti(L, -2, idx++); + } + + return 1; +} + +// mosis.apps.getInfo(package_id) -> {info} or nil +static int apps_getInfo(lua_State* L) { + if (!IsSystemApp(L)) { + return luaL_error(L, "mosis.apps.getInfo requires system permission"); + } + + const char* package_id = luaL_checkstring(L, 1); + + auto* manager = GetAppManager(L); + if (!manager) { + lua_pushnil(L); + return 1; + } + + auto app = manager->GetApp(package_id); + if (!app) { + lua_pushnil(L); + return 1; + } + + lua_createtable(L, 0, 12); + + lua_pushstring(L, app->package_id.c_str()); + lua_setfield(L, -2, "package_id"); + + lua_pushstring(L, app->name.c_str()); + lua_setfield(L, -2, "name"); + + lua_pushstring(L, app->version_name.c_str()); + lua_setfield(L, -2, "version_name"); + + lua_pushinteger(L, app->version_code); + lua_setfield(L, -2, "version_code"); + + lua_pushboolean(L, app->is_system_app); + lua_setfield(L, -2, "is_system_app"); + + lua_pushinteger(L, app->package_size); + lua_setfield(L, -2, "package_size"); + + lua_pushinteger(L, app->data_size); + lua_setfield(L, -2, "data_size"); + + lua_pushstring(L, app->icon_path.c_str()); + lua_setfield(L, -2, "icon"); + + lua_pushstring(L, app->developer_name.c_str()); + lua_setfield(L, -2, "developer"); + + // Permissions array + lua_createtable(L, static_cast(app->permissions.size()), 0); + int idx = 1; + for (const auto& perm : app->permissions) { + lua_pushstring(L, perm.c_str()); + lua_rawseti(L, -2, idx++); + } + lua_setfield(L, -2, "permissions"); + + return 1; +} + +// mosis.apps.install(url, signature, callback) +static int apps_install(lua_State* L) { + if (!IsSystemApp(L)) { + return luaL_error(L, "mosis.apps.install requires system permission"); + } + + const char* url = luaL_checkstring(L, 1); + const char* signature = lua_isstring(L, 2) ? lua_tostring(L, 2) : ""; + + // Callback is optional (argument 3) + bool has_callback = lua_isfunction(L, 3); + + auto* manager = GetAppManager(L); + if (!manager) { + lua_pushboolean(L, false); + return 1; + } + + if (has_callback) { + // Store callback reference + lua_pushvalue(L, 3); + int callback_ref = luaL_ref(L, LUA_REGISTRYINDEX); + + // Create progress callback that calls Lua + // Note: This is simplified - real implementation needs thread safety + ProgressCallback progress_cb = [L, callback_ref](const InstallProgress& progress) { + lua_rawgeti(L, LUA_REGISTRYINDEX, callback_ref); + + lua_createtable(L, 0, 3); + + lua_pushstring(L, InstallProgress::StageName(progress.stage)); + lua_setfield(L, -2, "stage"); + + lua_pushnumber(L, progress.progress); + lua_setfield(L, -2, "progress"); + + lua_pushstring(L, progress.error.c_str()); + lua_setfield(L, -2, "error"); + + if (lua_pcall(L, 1, 0, 0) != LUA_OK) { + LOG_ERROR("Install callback error: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + } + + // Clean up ref when complete + if (progress.stage == InstallProgress::Stage::Complete || + progress.stage == InstallProgress::Stage::Failed) { + luaL_unref(L, LUA_REGISTRYINDEX, callback_ref); + } + }; + + bool success = manager->Install(url, signature, progress_cb); + lua_pushboolean(L, success); + } else { + bool success = manager->Install(url, signature, nullptr); + lua_pushboolean(L, success); + } + + return 1; +} + +// mosis.apps.uninstall(package_id) -> boolean +static int apps_uninstall(lua_State* L) { + if (!IsSystemApp(L)) { + return luaL_error(L, "mosis.apps.uninstall requires system permission"); + } + + const char* package_id = luaL_checkstring(L, 1); + + auto* manager = GetAppManager(L); + if (!manager) { + lua_pushboolean(L, false); + return 1; + } + + bool success = manager->Uninstall(package_id); + lua_pushboolean(L, success); + return 1; +} + +// mosis.apps.launch(package_id) -> boolean +static int apps_launch(lua_State* L) { + if (!IsSystemApp(L)) { + return luaL_error(L, "mosis.apps.launch requires system permission"); + } + + const char* package_id = luaL_checkstring(L, 1); + + auto* manager = GetAppManager(L); + if (!manager) { + lua_pushboolean(L, false); + return 1; + } + + bool success = manager->LaunchApp(package_id); + lua_pushboolean(L, success); + return 1; +} + +// mosis.apps.getDataSize(package_id) -> number +static int apps_getDataSize(lua_State* L) { + if (!IsSystemApp(L)) { + return luaL_error(L, "mosis.apps.getDataSize requires system permission"); + } + + const char* package_id = luaL_checkstring(L, 1); + + auto* manager = GetAppManager(L); + if (!manager) { + lua_pushinteger(L, 0); + return 1; + } + + int64_t size = manager->GetAppDataSize(package_id); + lua_pushinteger(L, size); + return 1; +} + +// mosis.apps.clearCache(package_id) -> boolean +static int apps_clearCache(lua_State* L) { + if (!IsSystemApp(L)) { + return luaL_error(L, "mosis.apps.clearCache requires system permission"); + } + + const char* package_id = luaL_checkstring(L, 1); + + auto* manager = GetAppManager(L); + if (!manager) { + lua_pushboolean(L, false); + return 1; + } + + bool success = manager->ClearAppCache(package_id); + lua_pushboolean(L, success); + return 1; +} + +// mosis.apps.clearData(package_id) -> boolean +static int apps_clearData(lua_State* L) { + if (!IsSystemApp(L)) { + return luaL_error(L, "mosis.apps.clearData requires system permission"); + } + + const char* package_id = luaL_checkstring(L, 1); + + auto* manager = GetAppManager(L); + if (!manager) { + lua_pushboolean(L, false); + return 1; + } + + bool success = manager->ClearAppData(package_id); + lua_pushboolean(L, success); + return 1; +} + +// mosis.apps.checkUpdates(callback) -> void +static int apps_checkUpdates(lua_State* L) { + if (!IsSystemApp(L)) { + return luaL_error(L, "mosis.apps.checkUpdates requires system permission"); + } + + auto* service = GetUpdateService(L); + if (!service) { + lua_pushnil(L); + return 1; + } + + auto updates = service->CheckForUpdates(); + + lua_createtable(L, static_cast(updates.size()), 0); + int idx = 1; + + for (const auto& update : updates) { + lua_createtable(L, 0, 8); + + lua_pushstring(L, update.package_id.c_str()); + lua_setfield(L, -2, "package_id"); + + lua_pushstring(L, update.name.c_str()); + lua_setfield(L, -2, "name"); + + lua_pushstring(L, update.current_version.c_str()); + lua_setfield(L, -2, "current_version"); + + lua_pushstring(L, update.new_version.c_str()); + lua_setfield(L, -2, "new_version"); + + lua_pushinteger(L, update.download_size); + lua_setfield(L, -2, "size"); + + lua_pushstring(L, update.release_notes.c_str()); + lua_setfield(L, -2, "release_notes"); + + lua_pushboolean(L, update.is_critical); + lua_setfield(L, -2, "critical"); + + lua_rawseti(L, -2, idx++); + } + + return 1; +} + +// ============================================================================ +// mosis.app.* - All apps (info about current app) +// ============================================================================ + +// mosis.app.info() -> {package_id, name, version_name, version_code} +static int app_info(lua_State* L) { + std::string app_id = GetCurrentAppId(L); + if (app_id.empty()) { + lua_pushnil(L); + return 1; + } + + auto* manager = GetAppManager(L); + if (!manager) { + lua_pushnil(L); + return 1; + } + + auto app = manager->GetApp(app_id); + if (!app) { + lua_pushnil(L); + return 1; + } + + lua_createtable(L, 0, 4); + + lua_pushstring(L, app->package_id.c_str()); + lua_setfield(L, -2, "package_id"); + + lua_pushstring(L, app->name.c_str()); + lua_setfield(L, -2, "name"); + + lua_pushstring(L, app->version_name.c_str()); + lua_setfield(L, -2, "version_name"); + + lua_pushinteger(L, app->version_code); + lua_setfield(L, -2, "version_code"); + + return 1; +} + +// mosis.app.checkUpdate(callback) -> void +static int app_checkUpdate(lua_State* L) { + // Get callback + if (!lua_isfunction(L, 1)) { + return luaL_error(L, "callback function required"); + } + + std::string app_id = GetCurrentAppId(L); + auto* service = GetUpdateService(L); + + if (app_id.empty() || !service) { + // Call callback with no update + lua_pushvalue(L, 1); + lua_pushboolean(L, false); + lua_pushnil(L); + lua_pcall(L, 2, 0, 0); + return 0; + } + + auto updates = service->GetPendingUpdates(); + for (const auto& update : updates) { + if (update.package_id == app_id) { + // Call callback with update info + lua_pushvalue(L, 1); + lua_pushboolean(L, true); + lua_pushstring(L, update.new_version.c_str()); + lua_pcall(L, 2, 0, 0); + return 0; + } + } + + // No update available + lua_pushvalue(L, 1); + lua_pushboolean(L, false); + lua_pushnil(L); + lua_pcall(L, 2, 0, 0); + + return 0; +} + +// mosis.app.openStorePage() -> void +static int app_openStorePage(lua_State* L) { + std::string app_id = GetCurrentAppId(L); + if (app_id.empty()) { + return 0; + } + + // TODO: Navigate to store page for this app + // This would typically trigger navigation to: + // mosis://store/app/{app_id} + LOG_INFO("Open store page for: %s", app_id.c_str()); + + return 0; +} + +// ============================================================================ +// Registration +// ============================================================================ + +static const luaL_Reg apps_functions[] = { + {"getInstalled", apps_getInstalled}, + {"getInfo", apps_getInfo}, + {"install", apps_install}, + {"uninstall", apps_uninstall}, + {"launch", apps_launch}, + {"getDataSize", apps_getDataSize}, + {"clearCache", apps_clearCache}, + {"clearData", apps_clearData}, + {"checkUpdates", apps_checkUpdates}, + {nullptr, nullptr} +}; + +static const luaL_Reg app_functions[] = { + {"info", app_info}, + {"checkUpdate", app_checkUpdate}, + {"openStorePage", app_openStorePage}, + {nullptr, nullptr} +}; + +void RegisterAppAPIs(lua_State* L, + AppManager* app_manager, + UpdateService* update_service, + const std::string& current_app_id, + bool is_system_app) { + // Store pointers in registry + lua_pushlightuserdata(L, app_manager); + lua_setfield(L, LUA_REGISTRYINDEX, APP_MANAGER_KEY); + + lua_pushlightuserdata(L, update_service); + lua_setfield(L, LUA_REGISTRYINDEX, UPDATE_SERVICE_KEY); + + lua_pushstring(L, current_app_id.c_str()); + lua_setfield(L, LUA_REGISTRYINDEX, CURRENT_APP_ID_KEY); + + lua_pushboolean(L, is_system_app); + lua_setfield(L, LUA_REGISTRYINDEX, IS_SYSTEM_APP_KEY); + + // Get or create mosis table + lua_getglobal(L, "mosis"); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + lua_setglobal(L, "mosis"); + lua_getglobal(L, "mosis"); + } + + // Create mosis.apps table (system apps only) + if (is_system_app) { + lua_newtable(L); + luaL_setfuncs(L, apps_functions, 0); + lua_setfield(L, -2, "apps"); + } + + // Create mosis.app table (all apps) + lua_newtable(L); + luaL_setfuncs(L, app_functions, 0); + lua_setfield(L, -2, "app"); + + lua_pop(L, 1); // pop mosis table + + LOG_DEBUG("Registered app APIs for: %s (system=%d)", + current_app_id.c_str(), is_system_app); +} + +} // namespace mosis diff --git a/core/src/apps/app_manager.cpp b/core/src/apps/app_manager.cpp new file mode 100644 index 0000000..f4ff475 --- /dev/null +++ b/core/src/apps/app_manager.cpp @@ -0,0 +1,697 @@ +// app_manager.cpp - App installation and management implementation +// Milestone 10: Device-Side App Management + +#include "app_manager.h" +#include "../logger.h" + +#include "../sandbox/sandbox_manager.h" + +#include +#include +#include +#include +#include +#include + +// For JSON parsing +#include + +// For ZIP extraction +#include + +namespace fs = std::filesystem; +using json = nlohmann::json; + +namespace mosis { + +AppManager::AppManager(const std::string& data_root) + : m_data_root(data_root) +{ + // Create directory structure + fs::create_directories(m_data_root + "/apps"); + fs::create_directories(m_data_root + "/downloads"); + fs::create_directories(m_data_root + "/backups"); + fs::create_directories(m_data_root + "/config"); + + // Load installed apps registry + LoadInstalledApps(); + + LOG_INFO("AppManager initialized at: %s", m_data_root.c_str()); +} + +AppManager::~AppManager() { + SaveInstalledApps(); +} + +bool AppManager::Install(const std::string& package_url, + const std::string& signature, + ProgressCallback callback) { + callback({InstallProgress::Stage::Downloading, 0.0f, ""}); + + // Generate download path + std::string download_path = m_data_root + "/downloads/" + GenerateUUID() + ".mosis"; + + // Download package + if (!DownloadFile(package_url, download_path, [&](float p) { + callback({InstallProgress::Stage::Downloading, p, ""}); + })) { + callback({InstallProgress::Stage::Failed, 0.0f, "Download failed"}); + return false; + } + + callback({InstallProgress::Stage::Verifying, 0.0f, ""}); + + // Verify signature + if (!signature.empty() && !VerifySignature(download_path, signature)) { + fs::remove(download_path); + callback({InstallProgress::Stage::Failed, 0.0f, "Signature verification failed"}); + return false; + } + + // Verify package integrity + if (!VerifyPackage(download_path)) { + fs::remove(download_path); + callback({InstallProgress::Stage::Failed, 0.0f, "Package verification failed"}); + return false; + } + + // Continue with installation + bool result = InstallFromFile(download_path, callback); + + // Clean up download + fs::remove(download_path); + + return result; +} + +bool AppManager::InstallFromFile(const std::string& package_path, + ProgressCallback callback) { + callback({InstallProgress::Stage::Verifying, 0.0f, ""}); + + // Extract manifest to get package_id + auto manifest = ExtractManifest(package_path); + if (!manifest) { + callback({InstallProgress::Stage::Failed, 0.0f, "Invalid manifest"}); + return false; + } + + LOG_INFO("Installing app: %s v%s", manifest->id.c_str(), manifest->version.c_str()); + + callback({InstallProgress::Stage::Extracting, 0.0f, ""}); + + // Determine installation path + std::string install_path = m_data_root + "/apps/" + manifest->id; + + // Check if already installed (update path) + if (fs::exists(install_path + "/package")) { + LOG_INFO("App already installed, updating: %s", manifest->id.c_str()); + // Backup existing data + BackupAppData(manifest->id); + // Remove old package + fs::remove_all(install_path + "/package"); + } + + // Create directories + fs::create_directories(install_path + "/package"); + fs::create_directories(install_path + "/data"); + fs::create_directories(install_path + "/cache"); + fs::create_directories(install_path + "/db"); + + // Extract package + if (!ExtractPackage(package_path, install_path + "/package")) { + callback({InstallProgress::Stage::Failed, 0.0f, "Extraction failed"}); + return false; + } + + callback({InstallProgress::Stage::Registering, 0.0f, ""}); + + // Get package file size + int64_t package_size = 0; + try { + package_size = static_cast(fs::file_size(package_path)); + } catch (...) { + package_size = 0; + } + + // Register app + InstalledApp app; + app.package_id = manifest->id; + app.name = manifest->name; + app.version_name = manifest->version; + app.version_code = manifest->version_code; + app.install_path = install_path; + app.permissions = manifest->permissions; + app.installed_at = std::chrono::system_clock::now(); + app.updated_at = std::chrono::system_clock::now(); + app.package_size = package_size; + app.data_size = 0; + app.is_system_app = false; + app.entry_point = manifest->entry; + app.icon_path = manifest->icon; + app.developer_name = manifest->developer_name; + + { + std::lock_guard lock(m_mutex); + m_installed_apps[manifest->id] = app; + SaveInstalledApps(); + } + + LOG_INFO("App installed successfully: %s", manifest->id.c_str()); + callback({InstallProgress::Stage::Complete, 1.0f, ""}); + return true; +} + +bool AppManager::Uninstall(const std::string& package_id, bool keep_data) { + std::lock_guard lock(m_mutex); + + auto it = m_installed_apps.find(package_id); + if (it == m_installed_apps.end()) { + LOG_WARN("Cannot uninstall: app not found: %s", package_id.c_str()); + return false; + } + + // Cannot uninstall system apps + if (it->second.is_system_app) { + LOG_WARN("Cannot uninstall system app: %s", package_id.c_str()); + return false; + } + + LOG_INFO("Uninstalling app: %s (keep_data=%d)", package_id.c_str(), keep_data); + + // Stop app if running + if (m_sandbox_manager && m_sandbox_manager->IsAppRunning(package_id)) { + LOG_INFO("Stopping running app before uninstall: %s", package_id.c_str()); + m_sandbox_manager->StopApp(package_id); + } + + // Remove files + std::string install_path = it->second.install_path; + + try { + fs::remove_all(install_path + "/package"); + fs::remove_all(install_path + "/cache"); + + if (!keep_data) { + fs::remove_all(install_path + "/data"); + fs::remove_all(install_path + "/db"); + fs::remove_all(install_path); + } + } catch (const std::exception& e) { + LOG_ERROR("Error removing app files: %s", e.what()); + return false; + } + + // Unregister + m_installed_apps.erase(it); + SaveInstalledApps(); + + LOG_INFO("App uninstalled: %s", package_id.c_str()); + return true; +} + +bool AppManager::Update(const std::string& package_id, + const std::string& package_url, + const std::string& signature, + ProgressCallback callback) { + // Updates use the same flow as Install, which handles existing installations + return Install(package_url, signature, callback); +} + +std::vector AppManager::GetInstalledApps() const { + std::lock_guard lock(m_mutex); + + std::vector apps; + apps.reserve(m_installed_apps.size()); + for (const auto& [id, app] : m_installed_apps) { + apps.push_back(app); + } + return apps; +} + +std::optional AppManager::GetApp(const std::string& package_id) const { + std::lock_guard lock(m_mutex); + + auto it = m_installed_apps.find(package_id); + if (it != m_installed_apps.end()) { + return it->second; + } + return std::nullopt; +} + +bool AppManager::IsInstalled(const std::string& package_id) const { + std::lock_guard lock(m_mutex); + return m_installed_apps.find(package_id) != m_installed_apps.end(); +} + +int64_t AppManager::GetAppDataSize(const std::string& package_id) const { + std::string data_path = GetAppDataPath(package_id); + return CalculateDirectorySize(data_path); +} + +bool AppManager::ClearAppData(const std::string& package_id) { + std::string data_path = GetAppDataPath(package_id); + std::string db_path = m_data_root + "/apps/" + package_id + "/db"; + + try { + fs::remove_all(data_path); + fs::remove_all(db_path); + fs::create_directories(data_path); + fs::create_directories(db_path); + LOG_INFO("Cleared app data: %s", package_id.c_str()); + return true; + } catch (const std::exception& e) { + LOG_ERROR("Error clearing app data: %s", e.what()); + return false; + } +} + +bool AppManager::ClearAppCache(const std::string& package_id) { + std::string cache_path = GetAppCachePath(package_id); + + try { + fs::remove_all(cache_path); + fs::create_directories(cache_path); + LOG_INFO("Cleared app cache: %s", package_id.c_str()); + return true; + } catch (const std::exception& e) { + LOG_ERROR("Error clearing app cache: %s", e.what()); + return false; + } +} + +bool AppManager::BackupAppData(const std::string& package_id) { + std::string data_path = GetAppDataPath(package_id); + std::string backup_path = m_data_root + "/backups/" + package_id; + + try { + if (fs::exists(data_path)) { + fs::remove_all(backup_path); + fs::copy(data_path, backup_path, fs::copy_options::recursive); + LOG_INFO("Backed up app data: %s", package_id.c_str()); + } + return true; + } catch (const std::exception& e) { + LOG_ERROR("Error backing up app data: %s", e.what()); + return false; + } +} + +bool AppManager::RestoreAppData(const std::string& package_id) { + std::string data_path = GetAppDataPath(package_id); + std::string backup_path = m_data_root + "/backups/" + package_id; + + try { + if (fs::exists(backup_path)) { + fs::remove_all(data_path); + fs::copy(backup_path, data_path, fs::copy_options::recursive); + LOG_INFO("Restored app data: %s", package_id.c_str()); + } + return true; + } catch (const std::exception& e) { + LOG_ERROR("Error restoring app data: %s", e.what()); + return false; + } +} + +bool AppManager::LaunchApp(const std::string& package_id) { + auto app = GetApp(package_id); + if (!app) { + LOG_ERROR("Cannot launch app: not installed: %s", package_id.c_str()); + return false; + } + + if (!m_sandbox_manager) { + LOG_ERROR("Cannot launch app: sandbox manager not set"); + return false; + } + + if (m_sandbox_manager->IsAppRunning(package_id)) { + LOG_WARN("App already running: %s", package_id.c_str()); + return true; + } + + std::string app_path = app->install_path + "/package"; + LOG_INFO("Launching app: %s from %s", package_id.c_str(), app_path.c_str()); + + return m_sandbox_manager->StartApp(package_id, app_path, app->permissions, app->is_system_app); +} + +bool AppManager::StopApp(const std::string& package_id) { + if (!m_sandbox_manager) { + LOG_ERROR("Cannot stop app: sandbox manager not set"); + return false; + } + + if (!m_sandbox_manager->IsAppRunning(package_id)) { + LOG_WARN("App not running: %s", package_id.c_str()); + return true; + } + + LOG_INFO("Stopping app: %s", package_id.c_str()); + return m_sandbox_manager->StopApp(package_id); +} + +bool AppManager::IsAppRunning(const std::string& package_id) const { + if (!m_sandbox_manager) { + return false; + } + return m_sandbox_manager->IsAppRunning(package_id); +} + +void AppManager::SetSandboxManager(LuaSandboxManager* manager) { + m_sandbox_manager = manager; +} + +std::string AppManager::GetAppPath(const std::string& package_id) const { + return m_data_root + "/apps/" + package_id + "/package"; +} + +std::string AppManager::GetAppDataPath(const std::string& package_id) const { + return m_data_root + "/apps/" + package_id + "/data"; +} + +std::string AppManager::GetAppCachePath(const std::string& package_id) const { + return m_data_root + "/apps/" + package_id + "/cache"; +} + +void AppManager::RegisterSystemApp(const InstalledApp& app) { + std::lock_guard lock(m_mutex); + + InstalledApp system_app = app; + system_app.is_system_app = true; + m_installed_apps[app.package_id] = system_app; + + LOG_INFO("Registered system app: %s", app.package_id.c_str()); +} + +bool AppManager::VerifyPackage(const std::string& path) { + // Verify ZIP structure and manifest presence + unzFile zip = unzOpen(path.c_str()); + if (!zip) { + LOG_ERROR("Cannot open package: %s", path.c_str()); + return false; + } + + bool has_manifest = false; + + if (unzGoToFirstFile(zip) == UNZ_OK) { + do { + char filename[256]; + unz_file_info file_info; + if (unzGetCurrentFileInfo(zip, &file_info, filename, sizeof(filename), + nullptr, 0, nullptr, 0) == UNZ_OK) { + if (std::string(filename) == "manifest.json") { + has_manifest = true; + break; + } + } + } while (unzGoToNextFile(zip) == UNZ_OK); + } + + unzClose(zip); + + if (!has_manifest) { + LOG_ERROR("Package missing manifest.json: %s", path.c_str()); + return false; + } + + return true; +} + +bool AppManager::VerifySignature(const std::string& path, const std::string& signature) { + // TODO: Implement Ed25519 signature verification + // For now, accept packages without strict verification + LOG_WARN("Signature verification not yet implemented"); + return true; +} + +std::optional AppManager::ExtractManifest(const std::string& package_path) { + unzFile zip = unzOpen(package_path.c_str()); + if (!zip) { + return std::nullopt; + } + + std::string manifest_content; + + // Find and read manifest.json + if (unzLocateFile(zip, "manifest.json", 0) != UNZ_OK) { + unzClose(zip); + return std::nullopt; + } + + if (unzOpenCurrentFile(zip) != UNZ_OK) { + unzClose(zip); + return std::nullopt; + } + + char buffer[4096]; + int bytes_read; + while ((bytes_read = unzReadCurrentFile(zip, buffer, sizeof(buffer))) > 0) { + manifest_content.append(buffer, bytes_read); + } + + unzCloseCurrentFile(zip); + unzClose(zip); + + // Parse JSON + try { + json j = json::parse(manifest_content); + + AppManifest manifest; + manifest.id = j.value("id", ""); + manifest.name = j.value("name", ""); + manifest.version = j.value("version", "1.0.0"); + manifest.version_code = j.value("version_code", 1); + manifest.entry = j.value("entry", "main.rml"); + manifest.icon = j.value("icon", ""); + manifest.description = j.value("description", ""); + manifest.min_api_version = j.value("min_api_version", 1); + + if (j.contains("developer")) { + manifest.developer_name = j["developer"].value("name", ""); + manifest.developer_email = j["developer"].value("email", ""); + } + + if (j.contains("permissions") && j["permissions"].is_array()) { + for (const auto& perm : j["permissions"]) { + manifest.permissions.push_back(perm.get()); + } + } + + if (manifest.id.empty()) { + LOG_ERROR("Manifest missing required 'id' field"); + return std::nullopt; + } + + return manifest; + + } catch (const json::exception& e) { + LOG_ERROR("Failed to parse manifest: %s", e.what()); + return std::nullopt; + } +} + +bool AppManager::ExtractPackage(const std::string& package_path, const std::string& dest_path) { + unzFile zip = unzOpen(package_path.c_str()); + if (!zip) { + LOG_ERROR("Cannot open package for extraction: %s", package_path.c_str()); + return false; + } + + bool success = true; + + if (unzGoToFirstFile(zip) == UNZ_OK) { + do { + char filename[512]; + unz_file_info file_info; + + if (unzGetCurrentFileInfo(zip, &file_info, filename, sizeof(filename), + nullptr, 0, nullptr, 0) != UNZ_OK) { + continue; + } + + std::string full_path = dest_path + "/" + filename; + + // Skip META-INF directory (signatures) + if (std::string(filename).rfind("META-INF/", 0) == 0) { + continue; + } + + // Create directories + size_t len = strlen(filename); + if (len > 0 && filename[len - 1] == '/') { + fs::create_directories(full_path); + continue; + } + + // Ensure parent directory exists + fs::create_directories(fs::path(full_path).parent_path()); + + // Extract file + if (unzOpenCurrentFile(zip) != UNZ_OK) { + LOG_ERROR("Cannot open file in archive: %s", filename); + success = false; + break; + } + + std::ofstream out(full_path, std::ios::binary); + if (!out) { + LOG_ERROR("Cannot create file: %s", full_path.c_str()); + unzCloseCurrentFile(zip); + success = false; + break; + } + + char buffer[8192]; + int bytes_read; + while ((bytes_read = unzReadCurrentFile(zip, buffer, sizeof(buffer))) > 0) { + out.write(buffer, bytes_read); + } + + out.close(); + unzCloseCurrentFile(zip); + + } while (unzGoToNextFile(zip) == UNZ_OK); + } + + unzClose(zip); + return success; +} + +bool AppManager::DownloadFile(const std::string& url, const std::string& dest_path, + std::function progress_callback) { + // TODO: Implement HTTP download using platform-specific APIs + // For now, return false as this is a placeholder + LOG_ERROR("HTTP download not yet implemented for: %s", url.c_str()); + return false; +} + +void AppManager::LoadInstalledApps() { + std::string registry_path = m_data_root + "/config/apps.json"; + + std::ifstream file(registry_path); + if (!file) { + LOG_INFO("No existing app registry found"); + return; + } + + try { + json j; + file >> j; + + if (j.contains("apps") && j["apps"].is_array()) { + for (const auto& app_json : j["apps"]) { + InstalledApp app; + app.package_id = app_json.value("package_id", ""); + app.name = app_json.value("name", ""); + app.version_name = app_json.value("version_name", ""); + app.version_code = app_json.value("version_code", 0); + app.install_path = app_json.value("install_path", ""); + app.package_size = app_json.value("package_size", 0); + app.data_size = app_json.value("data_size", 0); + app.is_system_app = app_json.value("is_system_app", false); + app.entry_point = app_json.value("entry_point", "main.rml"); + app.icon_path = app_json.value("icon_path", ""); + app.developer_name = app_json.value("developer_name", ""); + + if (app_json.contains("permissions") && app_json["permissions"].is_array()) { + for (const auto& perm : app_json["permissions"]) { + app.permissions.push_back(perm.get()); + } + } + + // Parse timestamps + if (app_json.contains("installed_at")) { + auto ts = app_json["installed_at"].get(); + app.installed_at = std::chrono::system_clock::time_point( + std::chrono::seconds(ts)); + } + if (app_json.contains("updated_at")) { + auto ts = app_json["updated_at"].get(); + app.updated_at = std::chrono::system_clock::time_point( + std::chrono::seconds(ts)); + } + + if (!app.package_id.empty()) { + m_installed_apps[app.package_id] = app; + } + } + } + + LOG_INFO("Loaded %zu installed apps", m_installed_apps.size()); + + } catch (const std::exception& e) { + LOG_ERROR("Error loading app registry: %s", e.what()); + } +} + +void AppManager::SaveInstalledApps() { + std::string registry_path = m_data_root + "/config/apps.json"; + + json j; + j["version"] = 1; + j["apps"] = json::array(); + + for (const auto& [id, app] : m_installed_apps) { + json app_json; + app_json["package_id"] = app.package_id; + app_json["name"] = app.name; + app_json["version_name"] = app.version_name; + app_json["version_code"] = app.version_code; + app_json["install_path"] = app.install_path; + app_json["permissions"] = app.permissions; + app_json["package_size"] = app.package_size; + app_json["data_size"] = app.data_size; + app_json["is_system_app"] = app.is_system_app; + app_json["entry_point"] = app.entry_point; + app_json["icon_path"] = app.icon_path; + app_json["developer_name"] = app.developer_name; + + // Store timestamps as Unix seconds + app_json["installed_at"] = std::chrono::duration_cast( + app.installed_at.time_since_epoch()).count(); + app_json["updated_at"] = std::chrono::duration_cast( + app.updated_at.time_since_epoch()).count(); + + j["apps"].push_back(app_json); + } + + std::ofstream file(registry_path); + if (file) { + file << j.dump(2); + LOG_DEBUG("Saved app registry with %zu apps", m_installed_apps.size()); + } else { + LOG_ERROR("Failed to save app registry"); + } +} + +int64_t AppManager::CalculateDirectorySize(const std::string& path) const { + int64_t size = 0; + try { + for (const auto& entry : fs::recursive_directory_iterator(path)) { + if (entry.is_regular_file()) { + size += static_cast(entry.file_size()); + } + } + } catch (...) { + // Directory may not exist + } + return size; +} + +std::string AppManager::GenerateUUID() const { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> dis(0, 15); + + std::stringstream ss; + for (int i = 0; i < 32; ++i) { + if (i == 8 || i == 12 || i == 16 || i == 20) { + ss << '-'; + } + ss << std::hex << dis(gen); + } + return ss.str(); +} + +} // namespace mosis diff --git a/core/src/logger.cpp b/core/src/logger.cpp new file mode 100644 index 0000000..fec7fa1 --- /dev/null +++ b/core/src/logger.cpp @@ -0,0 +1,20 @@ +#include +#include + +#ifdef MOSIS_PLATFORM_ANDROID +#include +#define LOG_TAG "MosisOS" + +void Logger::Log(const std::string &message) +{ + __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "%s", message.c_str()); +} + +#else +// Desktop/other platforms - use stdout +void Logger::Log(const std::string &message) +{ + std::cout << message << std::endl; +} + +#endif diff --git a/core/src/sandbox/audit_log.cpp b/core/src/sandbox/audit_log.cpp new file mode 100644 index 0000000..0002311 --- /dev/null +++ b/core/src/sandbox/audit_log.cpp @@ -0,0 +1,188 @@ +#include "audit_log.h" + +#include + +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 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 AuditLog::GetEntries(size_t count) const { + std::lock_guard lock(m_mutex); + + std::vector 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 AuditLog::GetEntriesForApp(const std::string& app_id, + size_t count) const { + std::lock_guard lock(m_mutex); + + std::vector 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 AuditLog::GetEntriesByEvent(AuditEvent event, + size_t count) const { + std::lock_guard lock(m_mutex); + + std::vector 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 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 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 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 diff --git a/core/src/sandbox/crypto_api.cpp b/core/src/sandbox/crypto_api.cpp new file mode 100644 index 0000000..25eb49b --- /dev/null +++ b/core/src/sandbox/crypto_api.cpp @@ -0,0 +1,393 @@ +#include "crypto_api.h" + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#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 lock(m_mutex); + + std::string result(count, '\0'); + for (size_t i = 0; i < count; i++) { + result[i] = static_cast(m_gen() & 0xFF); + } + return result; +} + +int64_t SecureRandom::GetInt(int64_t min, int64_t max) { + std::lock_guard lock(m_mutex); + std::uniform_int_distribution dist(min, max); + return dist(m_gen); +} + +double SecureRandom::GetDouble() { + std::lock_guard lock(m_mutex); + std::uniform_real_distribution 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(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 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(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 hashBuffer(hashLength); + + status = BCryptCreateHash(hAlg, &hHash, nullptr, 0, + (PUCHAR)key.data(), static_cast(key.size()), 0); + if (!BCRYPT_SUCCESS(status)) { + BCryptCloseAlgorithmProvider(hAlg, 0); + return ""; + } + + status = BCryptHashData(hHash, (PUCHAR)data.data(), static_cast(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(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(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(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 diff --git a/core/src/sandbox/database_manager.cpp b/core/src/sandbox/database_manager.cpp new file mode 100644 index 0000000..a6a0d8b --- /dev/null +++ b/core/src/sandbox/database_manager.cpp @@ -0,0 +1,598 @@ +#include "database_manager.h" +#include +#include +#include +#include +#include + +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(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 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(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(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& params, std::string& error) { + sqlite3_stmt* stmt = static_cast(stmt_ptr); + + for (size_t i = 0; i < params.size(); i++) { + int idx = static_cast(i + 1); // SQLite parameters are 1-indexed + int rc = SQLITE_OK; + + std::visit([&](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + rc = sqlite3_bind_null(stmt, idx); + } else if constexpr (std::is_same_v) { + rc = sqlite3_bind_int64(stmt, idx, arg); + } else if constexpr (std::is_same_v) { + rc = sqlite3_bind_double(stmt, idx, arg); + } else if constexpr (std::is_same_v) { + rc = sqlite3_bind_text(stmt, idx, arg.c_str(), static_cast(arg.size()), SQLITE_TRANSIENT); + } else if constexpr (std::is_same_v>) { + rc = sqlite3_bind_blob(stmt, idx, arg.data(), static_cast(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& 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(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 DatabaseHandle::Query(const std::string& sql, const std::vector& 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(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(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(sqlite3_column_blob(stmt, i)); + int len = sqlite3_column_bytes(stmt, i); + row.push_back(std::vector(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 handle; +}; + +static int Lua_DatabaseHandle_Execute(lua_State* L) { + LuaDatabaseHandle* lh = static_cast(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 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(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(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(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 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(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(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(result->size()), 0); + int row_idx = 1; + for (const auto& row : *result) { + lua_createtable(L, static_cast(row.size()), 0); + int col_idx = 1; + for (const auto& val : row) { + std::visit([L](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + lua_pushnil(L); + } else if constexpr (std::is_same_v) { + lua_pushinteger(L, arg); + } else if constexpr (std::is_same_v) { + lua_pushnumber(L, arg); + } else if constexpr (std::is_same_v) { + lua_pushlstring(L, arg.c_str(), arg.size()); + } else if constexpr (std::is_same_v>) { + lua_pushlstring(L, reinterpret_cast(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(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(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(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(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(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(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 diff --git a/core/src/sandbox/http_validator.cpp b/core/src/sandbox/http_validator.cpp new file mode 100644 index 0000000..6a15647 --- /dev/null +++ b/core/src/sandbox/http_validator.cpp @@ -0,0 +1,388 @@ +#include "http_validator.h" +#include +#include +#include +#include + +namespace mosis { + +HttpValidator::HttpValidator() + : m_domain_restrictions_enabled(false) +{ +} + +void HttpValidator::SetAllowedDomains(const std::vector& 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 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(c))) return false; + } + int val = std::stoi(segment); + if (val < 0 || val > 255) return false; + if (i < host.length()) { + dots++; + num_start = static_cast(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 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 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 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(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(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 diff --git a/core/src/sandbox/json_api.cpp b/core/src/sandbox/json_api.cpp new file mode 100644 index 0000000..b635944 --- /dev/null +++ b/core/src/sandbox/json_api.cpp @@ -0,0 +1,369 @@ +#include "json_api.h" + +#include +#include +#include +#include + +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(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() ? 1 : 0); + break; + + case json::value_t::number_integer: + case json::value_t::number_unsigned: + lua_pushinteger(L, j.get()); + break; + + case json::value_t::number_float: + lua_pushnumber(L, j.get()); + break; + + case json::value_t::string: { + const std::string& s = j.get_ref(); + 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(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(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& visited, + size_t& output_size); + +static json LuaToJson(lua_State* L, int index, const JsonLimits& limits, + int depth, std::unordered_set& 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(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(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(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 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 diff --git a/core/src/sandbox/lua_sandbox.cpp b/core/src/sandbox/lua_sandbox.cpp new file mode 100644 index 0000000..60f9c4c --- /dev/null +++ b/core/src/sandbox/lua_sandbox.cpp @@ -0,0 +1,448 @@ +#include "lua_sandbox.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace mosis { + +//============================================================================= +// ALLOCATOR +//============================================================================= + +void* LuaSandbox::SandboxAlloc(void* ud, void* ptr, size_t osize, size_t nsize) { + auto* sandbox = static_cast(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(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(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(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 diff --git a/core/src/sandbox/network_manager.cpp b/core/src/sandbox/network_manager.cpp new file mode 100644 index 0000000..dbe3957 --- /dev/null +++ b/core/src/sandbox/network_manager.cpp @@ -0,0 +1,249 @@ +#include "network_manager.h" +#include +#include + +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& 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 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(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(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 diff --git a/core/src/sandbox/path_sandbox.cpp b/core/src/sandbox/path_sandbox.cpp new file mode 100644 index 0000000..562c33a --- /dev/null +++ b/core/src/sandbox/path_sandbox.cpp @@ -0,0 +1,344 @@ +#include "path_sandbox.h" + +#include +#include +#include +#include +#include + +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(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(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(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 diff --git a/core/src/sandbox/permission_gate.cpp b/core/src/sandbox/permission_gate.cpp new file mode 100644 index 0000000..517031f --- /dev/null +++ b/core/src/sandbox/permission_gate.cpp @@ -0,0 +1,197 @@ +#include "permission_gate.h" +#include "lua_sandbox.h" + +#include +#include + +namespace mosis { + +//============================================================================= +// PERMISSION DATABASE +//============================================================================= + +static const std::unordered_map 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(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& PermissionGate::GetDeclaredPermissions() const { + return m_context.permissions; +} + +std::vector PermissionGate::GetGrantedPermissions() const { + std::vector granted; + + for (const auto& perm : m_context.permissions) { + if (HasPermission(perm)) { + granted.push_back(perm); + } + } + + return granted; +} + +} // namespace mosis diff --git a/core/src/sandbox/rate_limiter.cpp b/core/src/sandbox/rate_limiter.cpp new file mode 100644 index 0000000..1eaf29a --- /dev/null +++ b/core/src/sandbox/rate_limiter.cpp @@ -0,0 +1,209 @@ +#include "rate_limiter.h" + +#include + +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 lock(m_mutex); + m_configs[operation] = config; +} + +const RateLimitConfig* RateLimiter::GetLimit(const std::string& operation) const { + std::lock_guard 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 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 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 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 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 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(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 diff --git a/core/src/sandbox/timer_manager.cpp b/core/src/sandbox/timer_manager.cpp new file mode 100644 index 0000000..a581bc4 --- /dev/null +++ b/core/src/sandbox/timer_manager.cpp @@ -0,0 +1,440 @@ +#include "timer_manager.h" + +#include +#include + +namespace mosis { + +//============================================================================= +// CONSTRUCTOR / DESTRUCTOR +//============================================================================= + +TimerManager::TimerManager() = default; + +TimerManager::~TimerManager() { + std::lock_guard 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 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 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 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 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 to_fire; + std::vector to_reschedule; + std::vector to_remove; + + auto now = std::chrono::steady_clock::now(); + + { + std::lock_guard 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 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 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(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(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(id)); + return 1; +} + +// clearTimeout(timer_id) +static int lua_clearTimeout(lua_State* L) { + TimerId id = static_cast(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(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(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(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(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 diff --git a/core/src/sandbox/virtual_fs.cpp b/core/src/sandbox/virtual_fs.cpp new file mode 100644 index 0000000..0b782fc --- /dev/null +++ b/core/src/sandbox/virtual_fs.cpp @@ -0,0 +1,706 @@ +#include "virtual_fs.h" + +#include +#include +#include +#include +#include +#include + +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(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 -> /data/foo.txt + // /cache/bar.txt -> /cache/bar.txt + // /temp/baz.txt -> /temp/baz.txt + // /shared/x.txt -> /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(-delta) > m_used_bytes) { + m_used_bytes = 0; + } else { + m_used_bytes = static_cast(static_cast(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 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(fs::file_size(physical_path, ec)); + } + + // Check quota for net change + int64_t delta = static_cast(data.size()) - static_cast(old_size); + if (delta > 0 && !CheckQuota(static_cast(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(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(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(fs::file_size(physical_path, ec)); + } + + if (!fs::remove(physical_path, ec)) { + error = "failed to delete"; + return false; + } + + UpdateUsage(-static_cast(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> 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 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 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(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(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(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(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(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(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(stat->size)); + lua_setfield(L, -2, "size"); + + lua_pushinteger(L, static_cast(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 diff --git a/designer/CMakeLists.txt b/designer/CMakeLists.txt index 2b06a6b..eea9955 100644 --- a/designer/CMakeLists.txt +++ b/designer/CMakeLists.txt @@ -30,7 +30,18 @@ FetchContent_MakeAvailable(rmlui) # Get glad include directories explicitly get_target_property(GLAD_INCLUDE_DIRS glad::glad INTERFACE_INCLUDE_DIRECTORIES) -# Shared kernel library (platform-agnostic code from MosisService) +#============================================================================== +# Mosis Core Library (shared sandbox APIs) +#============================================================================== +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../core ${CMAKE_CURRENT_BINARY_DIR}/mosis-core) + +# Point mosis-core to use the same Lua and JSON as designer +target_include_directories(mosis-core PUBLIC ${LUA_INCLUDE_DIR}) +target_link_libraries(mosis-core PUBLIC ${LUA_LIBRARIES}) + +#============================================================================== +# Mosis Kernel (RmlUi renderer - platform specific for now) +#============================================================================== add_library(mosis-kernel STATIC ../src/main/cpp/RmlUi_Renderer_GL3.cpp ) @@ -55,15 +66,9 @@ target_compile_definitions(mosis-kernel PUBLIC RMLUI_GL3_CUSTOM_LOADER="glad_loader.h" ) -# Sandbox sources (reuse from MosisService) -set(SANDBOX_SOURCES - ../src/main/cpp/sandbox/timer_manager.cpp - ../src/main/cpp/sandbox/json_api.cpp - ../src/main/cpp/sandbox/crypto_api.cpp - ../src/main/cpp/sandbox/virtual_fs.cpp -) - -# Designer executable +#============================================================================== +# Designer Executable +#============================================================================== add_executable(mosis-designer main.cpp src/desktop_platform.cpp @@ -79,8 +84,6 @@ add_executable(mosis-designer # Local backend with input recording hooks src/backend/RmlUi_Backend_GLFW_GL3.cpp src/backend/RmlUi_Platform_GLFW.cpp - # Sandbox APIs - ${SANDBOX_SOURCES} ) target_include_directories(mosis-designer PRIVATE @@ -89,11 +92,14 @@ target_include_directories(mosis-designer PRIVATE src/backend ../src/main/kernel/include ../src/main/cpp - ../src/main/cpp/sandbox + # Core library includes + ../core/include + ../core/include/mosis/sandbox ) target_link_libraries(mosis-designer PRIVATE mosis-kernel + mosis-core glad::glad glfw freetype