Initial commit

This commit is contained in:
DyadaMorgan
2026-09-22 21:04:05 +02:00
commit 4105cf3f9d
10 changed files with 4247 additions and 0 deletions
Generated Executable
+1046
View File
File diff suppressed because it is too large Load Diff
Executable
+22
View File
@@ -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"] }
+14
View File
@@ -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] "
}
]
+1
View File
@@ -0,0 +1 @@
[]
BIN
View File
Binary file not shown.
+14
View File
@@ -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
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
plugins = example
Executable
+929
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff