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