Initial commit
This commit is contained in:
+1046
File diff suppressed because it is too large
Load Diff
Executable
+22
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "openprivnet"
|
||||||
|
version = "0.9.8"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "server"
|
||||||
|
path = "src/server.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] }
|
||||||
|
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
|
||||||
|
rustls-pemfile = "2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||||
|
md5 = "0.7"
|
||||||
|
chrono = "0.4"
|
||||||
|
regex = "1"
|
||||||
|
once_cell = "1"
|
||||||
|
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"nick": "admin-nick",
|
||||||
|
"immunity": 999,
|
||||||
|
"prefix": "[&c&lADMIN&r] "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "192.168.0.1",
|
||||||
|
"nick": "helper-nick",
|
||||||
|
"immunity": 998,
|
||||||
|
"prefix": "[&c&lHELPER&r] "
|
||||||
|
}
|
||||||
|
]
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
Executable
BIN
Binary file not shown.
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"port": 12345,
|
||||||
|
"max_clients": 32,
|
||||||
|
"max_file_size": 9999,
|
||||||
|
"file_dir": "uploads",
|
||||||
|
"file_server_port": 7777,
|
||||||
|
"delete_files_on_shutdown": true,
|
||||||
|
"cert_file": "certs/server.crt",
|
||||||
|
"ca_file": "certs/ca.crt",
|
||||||
|
"server_pem": "certs/server_all.pem",
|
||||||
|
"key_file": "certs/server.key",
|
||||||
|
"welcome_text": "&2Welcome to OpenPrivNet! Type /nick <name> and /join <channel>."
|
||||||
|
}
|
||||||
Executable
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
def create_db():
|
||||||
|
conn = sqlite3.connect('db.sqlite')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Создание таблиц
|
||||||
|
cursor.execute('''CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
public_key TEXT NOT NULL
|
||||||
|
)''')
|
||||||
|
|
||||||
|
cursor.execute('''CREATE TABLE IF NOT EXISTS commands (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
command_name TEXT NOT NULL,
|
||||||
|
description TEXT
|
||||||
|
)''')
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
create_db()
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
plugins = example
|
||||||
Executable
+929
@@ -0,0 +1,929 @@
|
|||||||
|
// OpenPrivNet — Lua Plugin System
|
||||||
|
// Аналог PluginManager + PluginContext из Python-версии.
|
||||||
|
//
|
||||||
|
// API для плагинов (Lua):
|
||||||
|
// init_plugin(ctx) — обязательная функция инициализации
|
||||||
|
// cleanup_plugin() — опциональная функция выгрузки
|
||||||
|
//
|
||||||
|
// ctx — объект PluginContext со следующими методами:
|
||||||
|
// ctx:register_command("/cmd", function(client_id, args) ... end)
|
||||||
|
// ctx:register_event("client_connected", function(client_id) ... end)
|
||||||
|
// ctx:register_event("client_disconnected", function(client_id) ... end)
|
||||||
|
// ctx:register_event("message_sent", function(client_id, msg, formatted) ... end)
|
||||||
|
// ctx:register_event("channel_joined", function(client_id, channel) ... end)
|
||||||
|
// ctx:register_event("channel_left", function(client_id, channel) ... end)
|
||||||
|
// ctx:register_event("nickname_changed", function(client_id, old_nick, new_nick) ... end)
|
||||||
|
// ctx:register_event("file_uploaded", function(client_id, filename, size, url) ... end)
|
||||||
|
// ctx:register_event("file_downloaded", function(client_id, filename, size, url) ... end)
|
||||||
|
//
|
||||||
|
// ctx:send_message(client_id, message)
|
||||||
|
// ctx:broadcast_to_channel(channel_name, message)
|
||||||
|
// ctx:get_clients_in_channel(channel_name) -> table of client_id
|
||||||
|
// ctx:get_all_channels() -> table of channel names
|
||||||
|
// ctx:get_client_info(client_id) -> {nickname, channel, prefix, ip}
|
||||||
|
// ctx:get_client_by_nickname(nickname) -> client_id or nil
|
||||||
|
// ctx:create_channel(name) -> result string
|
||||||
|
// ctx:delete_channel(name) -> result string
|
||||||
|
// ctx:log(message) -> print with plugin prefix
|
||||||
|
//
|
||||||
|
// Безопасность (sandbox):
|
||||||
|
// - Заблокированы: os.execute, io.popen, load, loadfile, dofile, require,
|
||||||
|
// rawget, rawset, rawequal, rawlen, debug, package
|
||||||
|
// - Файловый I/O ограничен папкой plugins/plugins_data/<plugin_name>/
|
||||||
|
// - Доступ к certs/ заблокирован на уровне ядра sandbox'а
|
||||||
|
// - Нет доступа к сети напрямую (нет socket, нет ffi)
|
||||||
|
// - Лимит инструкций на один вызов (защита от бесконечных циклов)
|
||||||
|
// - Лимит памяти на Lua-состояние
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use mlua::prelude::*;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
use crate::{ClientId, SharedState};
|
||||||
|
|
||||||
|
// ─── Константы безопасности ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Максимум Lua-инструкций за один вызов (защита от while true do end)
|
||||||
|
const LUA_INSTRUCTION_LIMIT: u32 = 1_000_000;
|
||||||
|
|
||||||
|
/// Корневая директория для файлов плагинов
|
||||||
|
const PLUGIN_DATA_ROOT: &str = "plugins/plugins_data";
|
||||||
|
|
||||||
|
/// Запрещённые пути (блокируются на уровне ядра)
|
||||||
|
const BLOCKED_PATH_PREFIXES: &[&str] = &[
|
||||||
|
"certs/",
|
||||||
|
"certs\\",
|
||||||
|
"../certs",
|
||||||
|
"..\\certs",
|
||||||
|
];
|
||||||
|
|
||||||
|
// ─── Типы событий ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum PluginEvent {
|
||||||
|
ClientConnected,
|
||||||
|
ClientDisconnected,
|
||||||
|
MessageSent,
|
||||||
|
ChannelJoined,
|
||||||
|
ChannelLeft,
|
||||||
|
NicknameChanged,
|
||||||
|
FileUploaded,
|
||||||
|
FileDownloaded,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginEvent {
|
||||||
|
fn from_str(s: &str) -> Option<Self> {
|
||||||
|
match s {
|
||||||
|
"client_connected" => Some(Self::ClientConnected),
|
||||||
|
"client_disconnected" => Some(Self::ClientDisconnected),
|
||||||
|
"message_sent" => Some(Self::MessageSent),
|
||||||
|
"channel_joined" => Some(Self::ChannelJoined),
|
||||||
|
"channel_left" => Some(Self::ChannelLeft),
|
||||||
|
"nickname_changed" => Some(Self::NicknameChanged),
|
||||||
|
"file_uploaded" => Some(Self::FileUploaded),
|
||||||
|
"file_downloaded" => Some(Self::FileDownloaded),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::ClientConnected => "client_connected",
|
||||||
|
Self::ClientDisconnected => "client_disconnected",
|
||||||
|
Self::MessageSent => "message_sent",
|
||||||
|
Self::ChannelJoined => "channel_joined",
|
||||||
|
Self::ChannelLeft => "channel_left",
|
||||||
|
Self::NicknameChanged => "nickname_changed",
|
||||||
|
Self::FileUploaded => "file_uploaded",
|
||||||
|
Self::FileDownloaded => "file_downloaded",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Аргументы событий ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum EventArgs {
|
||||||
|
ClientConnected { client_id: ClientId },
|
||||||
|
ClientDisconnected { client_id: ClientId },
|
||||||
|
MessageSent { client_id: ClientId, msg: String, formatted: String },
|
||||||
|
ChannelJoined { client_id: ClientId, channel: String },
|
||||||
|
ChannelLeft { client_id: ClientId, channel: String },
|
||||||
|
NicknameChanged { client_id: ClientId, old_nick: String, new_nick: String },
|
||||||
|
FileUploaded { client_id: ClientId, filename: String, size: u64, url: String },
|
||||||
|
FileDownloaded { client_id: ClientId, filename: String, size: u64, url: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Канал для операций с сервером из Lua ─────────────────────────────────────
|
||||||
|
// Lua работает синхронно внутри mlua, поэтому для отправки сообщений клиентам
|
||||||
|
// используем mpsc — плагин кладёт задачи в очередь, tokio выполняет их асинхронно.
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub enum ServerOp {
|
||||||
|
SendToClient { client_id: ClientId, message: String },
|
||||||
|
BroadcastChannel { channel: String, message: String },
|
||||||
|
CreateChannel { name: String, reply: mpsc::Sender<String> },
|
||||||
|
DeleteChannel { name: String, reply: mpsc::Sender<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Состояние одного плагина ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
struct PluginState {
|
||||||
|
name: String,
|
||||||
|
loaded_at: f64,
|
||||||
|
/// Lua VM для этого плагина (изолирован от других плагинов)
|
||||||
|
lua: Lua,
|
||||||
|
/// Зарегистрированные команды: "/cmd" -> lua function key в registry
|
||||||
|
commands: Vec<String>,
|
||||||
|
/// Зарегистрированные события: event -> lua function key в registry
|
||||||
|
event_handlers: HashMap<PluginEvent, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── PluginManager ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub struct PluginManager {
|
||||||
|
plugins: HashMap<String, PluginState>,
|
||||||
|
/// Команды: "/cmd" -> plugin_name
|
||||||
|
plugin_commands: HashMap<String, String>,
|
||||||
|
/// Канал для передачи серверных операций из Lua → Tokio
|
||||||
|
server_op_tx: mpsc::UnboundedSender<ServerOp>,
|
||||||
|
/// Shared state для чтения (nickname lookup, channels и т.д.)
|
||||||
|
state: SharedState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginManager {
|
||||||
|
pub fn new(state: SharedState, server_op_tx: mpsc::UnboundedSender<ServerOp>) -> Self {
|
||||||
|
Self {
|
||||||
|
plugins: HashMap::new(),
|
||||||
|
plugin_commands: HashMap::new(),
|
||||||
|
server_op_tx,
|
||||||
|
state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Загрузка плагинов из plugins.cfg ──────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn load_plugins_from_config(&mut self) {
|
||||||
|
if !Path::new("plugins.cfg").exists() {
|
||||||
|
println!("[PluginManager] plugins.cfg not found, no plugins loaded.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = match fs::read_to_string("plugins.cfg") {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => { eprintln!("[PluginManager] Error reading plugins.cfg: {}", e); return; }
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut plugin_names: Vec<String> = Vec::new();
|
||||||
|
for line in content.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.starts_with("plugins") {
|
||||||
|
if let Some((_, rhs)) = line.split_once('=') {
|
||||||
|
plugin_names = rhs.split_whitespace().map(|s| s.to_string()).collect();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("[PluginManager] Loading plugins: {:?}", plugin_names);
|
||||||
|
let mut loaded = 0;
|
||||||
|
let total = plugin_names.len();
|
||||||
|
for name in plugin_names {
|
||||||
|
if self.load_plugin(&name) { loaded += 1; }
|
||||||
|
}
|
||||||
|
println!("[PluginManager] Loaded {}/{} plugins", loaded, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Загрузка одного плагина ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn load_plugin(&mut self, plugin_name: &str) -> bool {
|
||||||
|
let path = format!("plugins/{}.lua", plugin_name);
|
||||||
|
if !Path::new(&path).exists() {
|
||||||
|
eprintln!("[PluginManager] Plugin file not found: {}", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = match fs::read_to_string(&path) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => { eprintln!("[PluginManager] Cannot read {}: {}", path, e); return false; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Создаём изолированную Lua VM
|
||||||
|
let lua = match Lua::new_with(
|
||||||
|
LuaStdLib::TABLE | LuaStdLib::STRING | LuaStdLib::MATH | LuaStdLib::OS,
|
||||||
|
LuaOptions::default(),
|
||||||
|
) {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => { eprintln!("[PluginManager] Failed to create Lua state for '{}': {}", plugin_name, e); return false; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Применяем sandbox
|
||||||
|
if let Err(e) = Self::apply_sandbox(&lua, plugin_name, &self.server_op_tx, self.state.clone()) {
|
||||||
|
eprintln!("[PluginManager] Failed to sandbox '{}': {}", plugin_name, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём папку данных плагина
|
||||||
|
let data_dir = format!("{}/{}", PLUGIN_DATA_ROOT, plugin_name);
|
||||||
|
let _ = fs::create_dir_all(&data_dir);
|
||||||
|
|
||||||
|
// Устанавливаем лимит инструкций
|
||||||
|
let _ = lua.set_hook(LuaHookTriggers::default().every_nth_instruction(LUA_INSTRUCTION_LIMIT), |_lua, _debug| {
|
||||||
|
Err(LuaError::RuntimeError(
|
||||||
|
"Plugin exceeded instruction limit (possible infinite loop)".into()
|
||||||
|
))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Выполняем код плагина
|
||||||
|
if let Err(e) = lua.load(&code).set_name(plugin_name).exec() {
|
||||||
|
eprintln!("[PluginManager] Error executing plugin '{}': {}", plugin_name, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сбрасываем хук — init_plugin может быть длинным при первом запуске
|
||||||
|
lua.remove_hook();
|
||||||
|
|
||||||
|
// Ищем init_plugin
|
||||||
|
let init_fn: LuaFunction = match lua.globals().get("init_plugin") {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("[PluginManager] Plugin '{}' has no init_plugin function", plugin_name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Создаём PluginContext как Lua userdata/table
|
||||||
|
let ctx = match Self::make_plugin_context(&lua, plugin_name, &self.server_op_tx, self.state.clone()) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => { eprintln!("[PluginManager] Failed to create context for '{}': {}", plugin_name, e); return false; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Вызываем init_plugin(ctx)
|
||||||
|
let result: LuaResult<LuaValue> = init_fn.call(ctx);
|
||||||
|
match result {
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[PluginManager] Error in init_plugin of '{}': {}", plugin_name, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Ok(LuaValue::Boolean(false)) => {
|
||||||
|
eprintln!("[PluginManager] Plugin '{}' init_plugin returned false", plugin_name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Читаем зарегистрированные команды и события из специальной таблицы
|
||||||
|
// (они были записаны в _G._plugin_registry во время init_plugin)
|
||||||
|
let (commands, event_handlers) = Self::collect_registry(&lua, plugin_name);
|
||||||
|
|
||||||
|
// Регистрируем команды в глобальной таблице
|
||||||
|
for cmd in &commands {
|
||||||
|
if self.plugin_commands.contains_key(cmd) {
|
||||||
|
println!("[PluginManager] Warning: command {} already registered, overriding", cmd);
|
||||||
|
}
|
||||||
|
self.plugin_commands.insert(cmd.clone(), plugin_name.to_string());
|
||||||
|
println!("[+] Command {} registered by plugin {}", cmd, plugin_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let loaded_at = SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs_f64();
|
||||||
|
|
||||||
|
self.plugins.insert(plugin_name.to_string(), PluginState {
|
||||||
|
name: plugin_name.to_string(),
|
||||||
|
loaded_at,
|
||||||
|
lua,
|
||||||
|
commands,
|
||||||
|
event_handlers,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Восстанавливаем лимит инструкций для обычных вызовов
|
||||||
|
if let Some(ps) = self.plugins.get(&plugin_name.to_string()) {
|
||||||
|
ps.lua.set_hook(LuaHookTriggers::default().every_nth_instruction(LUA_INSTRUCTION_LIMIT), |_lua, _debug| {
|
||||||
|
Err(LuaError::RuntimeError("Plugin exceeded instruction limit".into()))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("[+] Plugin '{}' loaded successfully", plugin_name);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Выгрузка плагина ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn unload_plugin(&mut self, plugin_name: &str) -> bool {
|
||||||
|
let ps = match self.plugins.remove(plugin_name) {
|
||||||
|
Some(p) => p,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Вызываем cleanup_plugin() если есть
|
||||||
|
if let Ok(cleanup) = ps.lua.globals().get::<LuaFunction>("cleanup_plugin") {
|
||||||
|
if let Err(e) = cleanup.call::<()>(()) {
|
||||||
|
eprintln!("[PluginManager] Error in cleanup_plugin of '{}': {}", plugin_name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем команды
|
||||||
|
for cmd in &ps.commands {
|
||||||
|
self.plugin_commands.remove(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lua VM уничтожается вместе с ps
|
||||||
|
|
||||||
|
println!("[+] Plugin '{}' unloaded", plugin_name);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reload_plugin(&mut self, plugin_name: &str) -> bool {
|
||||||
|
println!("[PluginManager] Reloading plugin '{}'", plugin_name);
|
||||||
|
self.unload_plugin(plugin_name);
|
||||||
|
self.load_plugin(plugin_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reload_all_plugins(&mut self) {
|
||||||
|
println!("[PluginManager] Reloading all plugins...");
|
||||||
|
let names: Vec<String> = self.plugins.keys().cloned().collect();
|
||||||
|
for name in names {
|
||||||
|
self.unload_plugin(&name);
|
||||||
|
}
|
||||||
|
self.load_plugins_from_config();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Информация о плагинах ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn get_plugin_info(&self) -> Vec<PluginInfo> {
|
||||||
|
self.plugins.values().map(|ps| {
|
||||||
|
let ts = chrono::DateTime::from_timestamp(ps.loaded_at as i64, 0)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.format("%H:%M:%S")
|
||||||
|
.to_string();
|
||||||
|
PluginInfo {
|
||||||
|
name: ps.name.clone(),
|
||||||
|
loaded_at: ts,
|
||||||
|
commands: ps.commands.clone(),
|
||||||
|
}
|
||||||
|
}).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_command(&self, cmd: &str) -> bool {
|
||||||
|
self.plugin_commands.contains_key(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_plugin_commands(&self) -> Vec<(String, String)> {
|
||||||
|
self.plugin_commands.iter()
|
||||||
|
.map(|(cmd, plugin)| (cmd.clone(), plugin.clone()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Вызов команды плагина ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn dispatch_command(&self, command: &str, client_id: ClientId, args: &str) -> Option<String> {
|
||||||
|
let plugin_name = self.plugin_commands.get(command)?;
|
||||||
|
let ps = self.plugins.get(plugin_name)?;
|
||||||
|
|
||||||
|
// Читаем функцию из реестра плагина
|
||||||
|
let registry: LuaTable = match ps.lua.globals().get("_plugin_registry") {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(_) => return Some(format!("Plugin '{}' registry missing", plugin_name)),
|
||||||
|
};
|
||||||
|
let cmds: LuaTable = match registry.get("commands") {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(_) => return Some(format!("Plugin '{}' commands table missing", plugin_name)),
|
||||||
|
};
|
||||||
|
let handler: LuaFunction = match cmds.get(command.to_string()) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(_) => return Some(format!("Command handler not found for {}", command)),
|
||||||
|
};
|
||||||
|
|
||||||
|
match handler.call::<LuaValue>((client_id as LuaInteger, args.to_string())) {
|
||||||
|
Ok(LuaValue::String(s)) => Some(s.to_string_lossy().to_string()),
|
||||||
|
Ok(LuaValue::Nil) | Ok(LuaValue::Boolean(false)) => None,
|
||||||
|
Ok(_) => None,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[PluginManager] Error in command '{}' from '{}': {}", command, plugin_name, e);
|
||||||
|
Some(format!("Plugin error: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Вызов события ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn trigger_event(&self, event: &EventArgs) {
|
||||||
|
let event_type = match event {
|
||||||
|
EventArgs::ClientConnected { .. } => PluginEvent::ClientConnected,
|
||||||
|
EventArgs::ClientDisconnected { .. } => PluginEvent::ClientDisconnected,
|
||||||
|
EventArgs::MessageSent { .. } => PluginEvent::MessageSent,
|
||||||
|
EventArgs::ChannelJoined { .. } => PluginEvent::ChannelJoined,
|
||||||
|
EventArgs::ChannelLeft { .. } => PluginEvent::ChannelLeft,
|
||||||
|
EventArgs::NicknameChanged { .. } => PluginEvent::NicknameChanged,
|
||||||
|
EventArgs::FileUploaded { .. } => PluginEvent::FileUploaded,
|
||||||
|
EventArgs::FileDownloaded { .. } => PluginEvent::FileDownloaded,
|
||||||
|
};
|
||||||
|
|
||||||
|
for ps in self.plugins.values() {
|
||||||
|
let handler_key = match ps.event_handlers.get(&event_type) {
|
||||||
|
Some(k) => k.clone(),
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let registry: LuaTable = match ps.lua.globals().get("_plugin_registry") {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
let events: LuaTable = match registry.get("events") {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
let handler: LuaFunction = match events.get(handler_key) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = match event {
|
||||||
|
EventArgs::ClientConnected { client_id } =>
|
||||||
|
handler.call::<()>(*client_id as LuaInteger),
|
||||||
|
EventArgs::ClientDisconnected { client_id } =>
|
||||||
|
handler.call::<()>(*client_id as LuaInteger),
|
||||||
|
EventArgs::MessageSent { client_id, msg, formatted } =>
|
||||||
|
handler.call::<()>((*client_id as LuaInteger, msg.clone(), formatted.clone())),
|
||||||
|
EventArgs::ChannelJoined { client_id, channel } =>
|
||||||
|
handler.call::<()>((*client_id as LuaInteger, channel.clone())),
|
||||||
|
EventArgs::ChannelLeft { client_id, channel } =>
|
||||||
|
handler.call::<()>((*client_id as LuaInteger, channel.clone())),
|
||||||
|
EventArgs::NicknameChanged { client_id, old_nick, new_nick } =>
|
||||||
|
handler.call::<()>((*client_id as LuaInteger, old_nick.clone(), new_nick.clone())),
|
||||||
|
EventArgs::FileUploaded { client_id, filename, size, url } =>
|
||||||
|
handler.call::<()>((*client_id as LuaInteger, filename.clone(), *size as LuaInteger, url.clone())),
|
||||||
|
EventArgs::FileDownloaded { client_id, filename, size, url } =>
|
||||||
|
handler.call::<()>((*client_id as LuaInteger, filename.clone(), *size as LuaInteger, url.clone())),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
eprintln!("[PluginManager] Error in event '{}' handler of '{}': {}",
|
||||||
|
event_type.as_str(), ps.name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Sandbox: убираем опасные функции и настраиваем безопасный I/O ─────────
|
||||||
|
|
||||||
|
fn apply_sandbox(
|
||||||
|
lua: &Lua,
|
||||||
|
plugin_name: &str,
|
||||||
|
_server_op_tx: &mpsc::UnboundedSender<ServerOp>, // добавлен _
|
||||||
|
_state: SharedState, // добавлен _
|
||||||
|
) -> LuaResult<()> {
|
||||||
|
let globals = lua.globals();
|
||||||
|
|
||||||
|
// 1. Убираем опасные глобальные функции
|
||||||
|
let dangerous: &[&str] = &[
|
||||||
|
"load", "loadfile", "dofile", "require",
|
||||||
|
"rawget", "rawset", "rawequal", "rawlen",
|
||||||
|
"collectgarbage", "newproxy",
|
||||||
|
];
|
||||||
|
for name in dangerous {
|
||||||
|
globals.set(*name, LuaValue::Nil)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Убираем опасные модули полностью
|
||||||
|
globals.set("debug", LuaValue::Nil)?;
|
||||||
|
globals.set("package", LuaValue::Nil)?;
|
||||||
|
globals.set("io", LuaValue::Nil)?; // заменим ниже безопасной версией
|
||||||
|
globals.set("os", LuaValue::Nil)?; // заменим ниже безопасной версией
|
||||||
|
|
||||||
|
// 3. Безопасный os — только time, clock, date (без execute, exit, getenv и т.д.)
|
||||||
|
let safe_os = lua.create_table()?;
|
||||||
|
{
|
||||||
|
let lua_ref = lua;
|
||||||
|
safe_os.set("time", lua_ref.create_function(|_, ()| {
|
||||||
|
Ok(SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs() as LuaInteger)
|
||||||
|
})?)?;
|
||||||
|
safe_os.set("clock", lua_ref.create_function(|_, ()| {
|
||||||
|
Ok(SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs_f64())
|
||||||
|
})?)?;
|
||||||
|
safe_os.set("date", lua_ref.create_function(|_, fmt: Option<String>| {
|
||||||
|
let fmt = fmt.unwrap_or_else(|| "%Y-%m-%d %H:%M:%S".to_string());
|
||||||
|
Ok(chrono::Local::now().format(&fmt).to_string())
|
||||||
|
})?)?;
|
||||||
|
}
|
||||||
|
globals.set("os", safe_os)?;
|
||||||
|
|
||||||
|
// 4. Безопасный io — только файлы внутри plugins/plugins_data/<plugin_name>/
|
||||||
|
let data_root = format!("{}/{}", PLUGIN_DATA_ROOT, plugin_name);
|
||||||
|
|
||||||
|
let safe_io = lua.create_table()?;
|
||||||
|
{
|
||||||
|
// io.open(path, mode) — только внутри data_dir
|
||||||
|
let dr = data_root.clone();
|
||||||
|
safe_io.set("open", lua.create_function(move |lua_ctx, (rel_path, mode): (String, Option<String>)| {
|
||||||
|
// Проверяем путь
|
||||||
|
check_path_safety(&rel_path)?;
|
||||||
|
let full_path = Path::new(&dr).join(&rel_path);
|
||||||
|
// Нормализуем и проверяем что не вышли за пределы data_dir
|
||||||
|
let canonical_base = fs::canonicalize(&dr)
|
||||||
|
.map_err(|e| LuaError::RuntimeError(format!("Data dir error: {}", e)))?;
|
||||||
|
// Для несуществующего файла проверяем родителя
|
||||||
|
let canonical_full = if full_path.exists() {
|
||||||
|
fs::canonicalize(&full_path)
|
||||||
|
.map_err(|e| LuaError::RuntimeError(format!("Path error: {}", e)))?
|
||||||
|
} else {
|
||||||
|
let parent = full_path.parent().unwrap_or(&full_path);
|
||||||
|
let canon_parent = fs::canonicalize(parent)
|
||||||
|
.map_err(|e| LuaError::RuntimeError(format!("Path error: {}", e)))?;
|
||||||
|
canon_parent.join(full_path.file_name().unwrap_or_default())
|
||||||
|
};
|
||||||
|
|
||||||
|
if !canonical_full.starts_with(&canonical_base) {
|
||||||
|
return Err(LuaError::RuntimeError(
|
||||||
|
"Access denied: path is outside plugin data directory".into()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mode_str = mode.as_deref().unwrap_or("r");
|
||||||
|
let file_handle = lua_ctx.create_table()?;
|
||||||
|
|
||||||
|
match mode_str {
|
||||||
|
"r" | "rb" => {
|
||||||
|
let content = fs::read(&canonical_full)
|
||||||
|
.map_err(|e| LuaError::RuntimeError(format!("Cannot read file: {}", e)))?;
|
||||||
|
if mode_str == "rb" {
|
||||||
|
file_handle.set("content", lua_ctx.create_string(&content)?)?;
|
||||||
|
} else {
|
||||||
|
let text = String::from_utf8_lossy(&content).to_string();
|
||||||
|
file_handle.set("content", text)?;
|
||||||
|
}
|
||||||
|
file_handle.set("pos", 0i64)?;
|
||||||
|
file_handle.set("mode", "r")?;
|
||||||
|
// file:read(fmt) — только "*a" и "*l"
|
||||||
|
let content_clone = String::from_utf8_lossy(
|
||||||
|
&fs::read(&canonical_full).unwrap_or_default()
|
||||||
|
).to_string();
|
||||||
|
file_handle.set("read", lua_ctx.create_function(move |_, (_, fmt): (LuaValue, Option<String>)| {
|
||||||
|
match fmt.as_deref().unwrap_or("*l") {
|
||||||
|
"*a" | "*all" => Ok(Some(content_clone.clone())),
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
})?)?;
|
||||||
|
file_handle.set("close", lua_ctx.create_function(|_, _: LuaValue| Ok(()))?)?;
|
||||||
|
}
|
||||||
|
"w" | "wb" | "a" | "ab" => {
|
||||||
|
let path_for_write = canonical_full.clone();
|
||||||
|
let append = mode_str.starts_with('a');
|
||||||
|
file_handle.set("mode", mode_str)?;
|
||||||
|
file_handle.set("_path", canonical_full.to_string_lossy().to_string())?;
|
||||||
|
file_handle.set("_buf", "")?;
|
||||||
|
file_handle.set("write", lua_ctx.create_function(move |_, (tbl, data): (LuaTable, String)| {
|
||||||
|
let cur: String = tbl.get("_buf").unwrap_or_default();
|
||||||
|
tbl.set("_buf", cur + &data)?;
|
||||||
|
Ok(())
|
||||||
|
})?)?;
|
||||||
|
let path_for_close = path_for_write.clone();
|
||||||
|
file_handle.set("close", lua_ctx.create_function(move |_, tbl: LuaTable| {
|
||||||
|
let buf: String = tbl.get("_buf").unwrap_or_default();
|
||||||
|
if append {
|
||||||
|
use std::io::Write;
|
||||||
|
let mut f = std::fs::OpenOptions::new()
|
||||||
|
.append(true).create(true).open(&path_for_close)
|
||||||
|
.map_err(|e| LuaError::RuntimeError(e.to_string()))?;
|
||||||
|
f.write_all(buf.as_bytes())
|
||||||
|
.map_err(|e| LuaError::RuntimeError(e.to_string()))?;
|
||||||
|
} else {
|
||||||
|
fs::write(&path_for_close, buf.as_bytes())
|
||||||
|
.map_err(|e| LuaError::RuntimeError(e.to_string()))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})?)?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(LuaError::RuntimeError(format!("Unsupported file mode: {}", mode_str)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(LuaValue::Table(file_handle))
|
||||||
|
})?)?;
|
||||||
|
|
||||||
|
// io.lines(path) — читает файл, возвращает таблицу строк
|
||||||
|
let dr2 = data_root.clone();
|
||||||
|
safe_io.set("lines", lua.create_function(move |lua_ctx, rel_path: String| {
|
||||||
|
check_path_safety(&rel_path)?;
|
||||||
|
let full_path = Path::new(&dr2).join(&rel_path);
|
||||||
|
let canonical_base = fs::canonicalize(&dr2)
|
||||||
|
.map_err(|e| LuaError::RuntimeError(format!("Data dir error: {}", e)))?;
|
||||||
|
let canonical_full = fs::canonicalize(&full_path)
|
||||||
|
.map_err(|e| LuaError::RuntimeError(format!("Path error: {}", e)))?;
|
||||||
|
if !canonical_full.starts_with(&canonical_base) {
|
||||||
|
return Err(LuaError::RuntimeError("Access denied".into()));
|
||||||
|
}
|
||||||
|
let content = fs::read_to_string(&canonical_full)
|
||||||
|
.map_err(|e| LuaError::RuntimeError(e.to_string()))?;
|
||||||
|
let tbl = lua_ctx.create_table()?;
|
||||||
|
for (i, line) in content.lines().enumerate() {
|
||||||
|
tbl.set(i + 1, line.to_string())?;
|
||||||
|
}
|
||||||
|
Ok(tbl)
|
||||||
|
})?)?;
|
||||||
|
}
|
||||||
|
globals.set("io", safe_io)?;
|
||||||
|
|
||||||
|
// 5. Инициализируем реестр команд и событий (пишется из ctx методов)
|
||||||
|
let registry = lua.create_table()?;
|
||||||
|
registry.set("commands", lua.create_table()?)?;
|
||||||
|
registry.set("events", lua.create_table()?)?;
|
||||||
|
globals.set("_plugin_registry", registry)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Создаём PluginContext для Lua ──────────────────────────────────────────
|
||||||
|
|
||||||
|
fn make_plugin_context(
|
||||||
|
lua: &Lua,
|
||||||
|
plugin_name: &str,
|
||||||
|
server_op_tx: &mpsc::UnboundedSender<ServerOp>,
|
||||||
|
state: SharedState,
|
||||||
|
) -> LuaResult<LuaTable> {
|
||||||
|
let ctx = lua.create_table()?;
|
||||||
|
let pname = plugin_name.to_string();
|
||||||
|
|
||||||
|
// ctx:register_command("/cmd", function(client_id, args) ... end)
|
||||||
|
{
|
||||||
|
let pn = pname.clone();
|
||||||
|
ctx.set("register_command", lua.create_function(move |lua_ctx, (_, cmd, handler): (LuaValue, String, LuaFunction)| {
|
||||||
|
if !cmd.starts_with('/') {
|
||||||
|
return Err(LuaError::RuntimeError("Command must start with '/'".into()));
|
||||||
|
}
|
||||||
|
// Валидация имени команды
|
||||||
|
if cmd.len() > 32 || !cmd.chars().all(|c| c.is_alphanumeric() || c == '/' || c == '_') {
|
||||||
|
return Err(LuaError::RuntimeError("Invalid command name".into()));
|
||||||
|
}
|
||||||
|
let registry: LuaTable = lua_ctx.globals().get("_plugin_registry")?;
|
||||||
|
let cmds: LuaTable = registry.get("commands")?;
|
||||||
|
if cmds.contains_key(cmd.clone())? {
|
||||||
|
eprintln!("[Plugin:{}] Warning: command {} already registered, overriding", pn, cmd);
|
||||||
|
}
|
||||||
|
cmds.set(cmd.clone(), handler)?;
|
||||||
|
println!("[+] Command {} registered by plugin {}", cmd, pn);
|
||||||
|
Ok(())
|
||||||
|
})?)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctx:register_event("event_name", function(...) ... end)
|
||||||
|
{
|
||||||
|
let pn = pname.clone();
|
||||||
|
ctx.set("register_event", lua.create_function(move |lua_ctx, (_, event_name, handler): (LuaValue, String, LuaFunction)| {
|
||||||
|
if PluginEvent::from_str(&event_name).is_none() {
|
||||||
|
return Err(LuaError::RuntimeError(format!("Unknown event: {}", event_name)));
|
||||||
|
}
|
||||||
|
let registry: LuaTable = lua_ctx.globals().get("_plugin_registry")?;
|
||||||
|
let events: LuaTable = registry.get("events")?;
|
||||||
|
events.set(event_name.clone(), handler)?;
|
||||||
|
println!("[+] Event '{}' registered by plugin {}", event_name, pn);
|
||||||
|
Ok(())
|
||||||
|
})?)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctx:send_message(client_id, message)
|
||||||
|
{
|
||||||
|
let tx = server_op_tx.clone();
|
||||||
|
ctx.set("send_message", lua.create_function(move |_, (_, client_id, message): (LuaValue, LuaInteger, String)| {
|
||||||
|
let _ = tx.send(ServerOp::SendToClient { client_id: client_id as ClientId, message });
|
||||||
|
Ok(())
|
||||||
|
})?)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctx:broadcast_to_channel(channel_name, message)
|
||||||
|
{
|
||||||
|
let tx = server_op_tx.clone();
|
||||||
|
ctx.set("broadcast_to_channel", lua.create_function(move |_, (_, channel, message): (LuaValue, String, String)| {
|
||||||
|
let _ = tx.send(ServerOp::BroadcastChannel { channel, message });
|
||||||
|
Ok(())
|
||||||
|
})?)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctx:get_client_info(client_id) -> {nickname, channel, prefix, ip}
|
||||||
|
{
|
||||||
|
let st = state.clone();
|
||||||
|
ctx.set("get_client_info", lua.create_function(move |lua_ctx, (_, client_id): (LuaValue, LuaInteger)| {
|
||||||
|
let state = tokio::task::block_in_place(|| st.blocking_read());
|
||||||
|
let info = lua_ctx.create_table()?;
|
||||||
|
if let Some(arc) = state.clients.get(&(client_id as ClientId)) {
|
||||||
|
if let Ok(c) = arc.try_lock() {
|
||||||
|
info.set("nickname", c.nickname.clone().unwrap_or_default())?;
|
||||||
|
info.set("channel", c.channel.clone().unwrap_or_default())?;
|
||||||
|
info.set("prefix", c.prefix.clone())?;
|
||||||
|
info.set("ip", c.addr.ip().to_string())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(info)
|
||||||
|
})?)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctx:get_clients_in_channel(channel_name) -> table of client_id
|
||||||
|
{
|
||||||
|
let st = state.clone();
|
||||||
|
ctx.set("get_clients_in_channel", lua.create_function(move |lua_ctx, (_, channel): (LuaValue, String)| {
|
||||||
|
let state = tokio::task::block_in_place(|| st.blocking_read());
|
||||||
|
let tbl = lua_ctx.create_table()?;
|
||||||
|
if let Some(ids) = state.channels.get(&channel) {
|
||||||
|
for (i, &id) in ids.iter().enumerate() {
|
||||||
|
tbl.set(i + 1, id as LuaInteger)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(tbl)
|
||||||
|
})?)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctx:get_all_channels() -> table of channel_name strings
|
||||||
|
{
|
||||||
|
let st = state.clone();
|
||||||
|
ctx.set("get_all_channels", lua.create_function(move |lua_ctx, _: LuaValue| {
|
||||||
|
let state = tokio::task::block_in_place(|| st.blocking_read());
|
||||||
|
let tbl = lua_ctx.create_table()?;
|
||||||
|
for (i, name) in state.channels.keys().enumerate() {
|
||||||
|
tbl.set(i + 1, name.clone())?;
|
||||||
|
}
|
||||||
|
Ok(tbl)
|
||||||
|
})?)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctx:get_client_by_nickname(nickname) -> client_id or nil
|
||||||
|
{
|
||||||
|
let st = state.clone();
|
||||||
|
ctx.set("get_client_by_nickname", lua.create_function(move |_, (_, nickname): (LuaValue, String)| {
|
||||||
|
let state = tokio::task::block_in_place(|| st.blocking_read());
|
||||||
|
for (&id, arc) in &state.clients {
|
||||||
|
if let Ok(c) = arc.try_lock() {
|
||||||
|
if c.nickname.as_deref().map(|n| n.to_lowercase()) == Some(nickname.to_lowercase()) {
|
||||||
|
return Ok(LuaValue::Integer(id as LuaInteger));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(LuaValue::Nil)
|
||||||
|
})?)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctx:create_channel(name) -> result_string
|
||||||
|
{
|
||||||
|
let tx = server_op_tx.clone();
|
||||||
|
ctx.set("create_channel", lua.create_function(move |_, (_, name): (LuaValue, String)| {
|
||||||
|
let (reply_tx, mut reply_rx) = mpsc::channel::<String>(1);
|
||||||
|
let _ = tx.send(ServerOp::CreateChannel { name, reply: reply_tx });
|
||||||
|
// Синхронно ждём ответ (блокируем поток Lua, который и так не async)
|
||||||
|
let result = tokio::task::block_in_place(|| reply_rx.blocking_recv()).unwrap_or_else(|| "No response".to_string());
|
||||||
|
Ok(result)
|
||||||
|
})?)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctx:delete_channel(name) -> result_string
|
||||||
|
{
|
||||||
|
let tx = server_op_tx.clone();
|
||||||
|
ctx.set("delete_channel", lua.create_function(move |_, (_, name): (LuaValue, String)| {
|
||||||
|
let (reply_tx, mut reply_rx) = mpsc::channel::<String>(1);
|
||||||
|
let _ = tx.send(ServerOp::DeleteChannel { name, reply: reply_tx });
|
||||||
|
let result = tokio::task::block_in_place(|| reply_rx.blocking_recv()).unwrap_or_else(|| "No response".to_string());
|
||||||
|
Ok(result)
|
||||||
|
})?)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctx:log(message) — вывод с префиксом плагина
|
||||||
|
{
|
||||||
|
let pn = pname.clone();
|
||||||
|
ctx.set("log", lua.create_function(move |_, (_, msg): (LuaValue, String)| {
|
||||||
|
println!("[Plugin:{}] {}", pn, msg);
|
||||||
|
Ok(())
|
||||||
|
})?)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctx.name — имя плагина (read-only строка)
|
||||||
|
ctx.set("name", pname.clone())?;
|
||||||
|
|
||||||
|
// Все методы уже установлены напрямую в ctx через ctx.set(...),
|
||||||
|
// поэтому метатаблица не нужна — ctx:method() работает без неё.
|
||||||
|
|
||||||
|
Ok(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Собираем зарегистрированные команды и события из реестра ──────────────
|
||||||
|
|
||||||
|
fn collect_registry(lua: &Lua, _plugin_name: &str) -> (Vec<String>, HashMap<PluginEvent, String>) {
|
||||||
|
let mut commands = Vec::new();
|
||||||
|
let mut event_handlers = HashMap::new();
|
||||||
|
|
||||||
|
let registry: LuaTable = match lua.globals().get("_plugin_registry") {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(_) => return (commands, event_handlers),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Команды
|
||||||
|
if let Ok(cmds) = registry.get::<LuaTable>("commands") {
|
||||||
|
for pair in cmds.pairs::<String, LuaValue>() {
|
||||||
|
if let Ok((cmd, _)) = pair {
|
||||||
|
commands.push(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// События
|
||||||
|
if let Ok(events) = registry.get::<LuaTable>("events") {
|
||||||
|
for pair in events.pairs::<String, LuaValue>() {
|
||||||
|
if let Ok((event_name, _)) = pair {
|
||||||
|
if let Some(ev) = PluginEvent::from_str(&event_name) {
|
||||||
|
event_handlers.insert(ev, event_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(commands, event_handlers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Проверка безопасности пути ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn check_path_safety(path: &str) -> LuaResult<()> {
|
||||||
|
// Запрещаем абсолютные пути
|
||||||
|
if path.starts_with('/') || path.starts_with('\\') {
|
||||||
|
return Err(LuaError::RuntimeError("Absolute paths are not allowed".into()));
|
||||||
|
}
|
||||||
|
// Запрещаем path traversal
|
||||||
|
if path.contains("..") {
|
||||||
|
return Err(LuaError::RuntimeError("Path traversal ('..') is not allowed".into()));
|
||||||
|
}
|
||||||
|
// Запрещаем доступ к сертификатам
|
||||||
|
for blocked in BLOCKED_PATH_PREFIXES {
|
||||||
|
if path.to_lowercase().contains(blocked) {
|
||||||
|
return Err(LuaError::RuntimeError("Access to certificate files is forbidden".into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Запрещаем NULL-байты
|
||||||
|
if path.contains('\0') {
|
||||||
|
return Err(LuaError::RuntimeError("Invalid path".into()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Публичные типы ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PluginInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub loaded_at: String,
|
||||||
|
pub commands: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Задача-обработчик ServerOp (запускается в tokio) ─────────────────────────
|
||||||
|
|
||||||
|
pub async fn run_server_op_handler(
|
||||||
|
mut rx: mpsc::UnboundedReceiver<ServerOp>,
|
||||||
|
state: SharedState,
|
||||||
|
) {
|
||||||
|
while let Some(op) = rx.recv().await {
|
||||||
|
match op {
|
||||||
|
ServerOp::SendToClient { client_id, message } => {
|
||||||
|
let state = state.read().await;
|
||||||
|
if let Some(arc) = state.clients.get(&client_id) {
|
||||||
|
if let Ok(c) = arc.try_lock() {
|
||||||
|
let _ = c.tx.try_send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ServerOp::BroadcastChannel { channel, message } => {
|
||||||
|
let state = state.read().await;
|
||||||
|
if let Some(ids) = state.channels.get(&channel) {
|
||||||
|
for &id in ids {
|
||||||
|
if let Some(arc) = state.clients.get(&id) {
|
||||||
|
if let Ok(c) = arc.try_lock() {
|
||||||
|
let _ = c.tx.try_send(message.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ServerOp::CreateChannel { name, reply } => {
|
||||||
|
let result = {
|
||||||
|
let mut state = state.write().await;
|
||||||
|
crate::create_channel(&mut state, &name)
|
||||||
|
};
|
||||||
|
let _ = reply.send(result).await;
|
||||||
|
}
|
||||||
|
ServerOp::DeleteChannel { name, reply } => {
|
||||||
|
let result = {
|
||||||
|
let mut state = state.write().await;
|
||||||
|
crate::delete_channel(&mut state, &name)
|
||||||
|
};
|
||||||
|
let _ = reply.send(result).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+2197
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user