#include "naut/script.h" #include #include #include #include #include #include #include #include #include typedef struct { naut_event event; char message[256]; char path[PATH_MAX]; } script_job; struct naut_script { naut_event_bus *events; uint64_t subscription; lua_State *lua; pthread_t thread; pthread_mutex_t lock; pthread_cond_t ready; script_job *queue; size_t capacity; size_t head; size_t count; bool stopping; naut_script_move_file_cb move_file; void *move_context; _Atomic uint64_t queued; _Atomic uint64_t handled; _Atomic uint64_t dropped; _Atomic uint64_t errors; _Atomic uint64_t move_requests; char last_error[256]; }; static const char *hook_names[] = { [NAUT_EVENT_TORRENT_ADDED] = "on_torrent_added", [NAUT_EVENT_PIECE_COMPLETE] = "on_piece_complete", [NAUT_EVENT_FILE_COMPLETE] = "on_file_complete", [NAUT_EVENT_TORRENT_FINISHED] = "on_torrent_finished", [NAUT_EVENT_PEER_CONNECTED] = "on_peer_connected", [NAUT_EVENT_ALERT] = "on_alert", }; static void set_last_error(naut_script *script, const char *message) { pthread_mutex_lock(&script->lock); snprintf(script->last_error, sizeof script->last_error, "%s", message ? message : "unknown Lua error"); pthread_mutex_unlock(&script->lock); } static naut_script *lua_script(lua_State *lua) { return lua_touserdata(lua, lua_upvalueindex(1)); } static int lua_move_file(lua_State *lua) { naut_script *script = lua_script(lua); lua_Integer torrent_id = luaL_checkinteger(lua, 1); lua_Integer file_index = luaL_checkinteger(lua, 2); const char *destination = luaL_checkstring(lua, 3); if (torrent_id < 0 || file_index < 0 || (uint64_t)file_index > UINT32_MAX) return luaL_error(lua, "move_file arguments out of range"); if (!script->move_file) return luaL_error(lua, "move_file is unavailable"); naut_err error = script->move_file(script->move_context, (uint64_t)torrent_id, (uint32_t)file_index, destination); if (error != NAUT_OK) return luaL_error(lua, "move_file failed: %d", error); atomic_fetch_add_explicit(&script->move_requests, 1, memory_order_relaxed); return 0; } static void sandbox(lua_State *lua) { /* Remove every documented route to the filesystem, subprocesses, native * module loading, and raw chunk compilation. `load`/`loadstring` are * blocked too: with the default "bt" mode they accept *binary* chunks, and * a crafted bytecode chunk can escape the VM entirely — so even though * scripts are operator-supplied, we deny the bytecode-loader as * defense-in-depth. */ static const char *blocked[] = { "debug", "dofile", "io", "load", "loadfile", "loadstring", "os", "package", "require", }; for (size_t i = 0; i < NAUT_ARRAY_LEN(blocked); i++) { lua_pushnil(lua); lua_setglobal(lua, blocked[i]); } } static void install_api(naut_script *script) { lua_State *lua = script->lua; lua_newtable(lua); lua_pushlightuserdata(lua, script); lua_pushcclosure(lua, lua_move_file, 1); lua_setfield(lua, -2, "move_file"); lua_setglobal(lua, "naut"); } static void push_event(lua_State *lua, const naut_event *event) { lua_createtable(lua, 0, 5); lua_pushstring(lua, naut_event_type_name(event->type)); lua_setfield(lua, -2, "type"); lua_pushinteger(lua, (lua_Integer)event->torrent_id); lua_setfield(lua, -2, "torrent_id"); lua_pushinteger(lua, (lua_Integer)event->index); lua_setfield(lua, -2, "index"); if (event->message) { lua_pushstring(lua, event->message); lua_setfield(lua, -2, "message"); } if (event->path) { lua_pushstring(lua, event->path); lua_setfield(lua, -2, "path"); } } static void run_hook(naut_script *script, const naut_event *event) { if ((size_t)event->type >= NAUT_ARRAY_LEN(hook_names)) { atomic_fetch_add_explicit(&script->errors, 1, memory_order_relaxed); set_last_error(script, "unknown event type"); return; } const char *hook = hook_names[event->type]; lua_getglobal(script->lua, hook); if (lua_isnil(script->lua, -1)) { lua_pop(script->lua, 1); return; } if (!lua_isfunction(script->lua, -1)) { lua_pop(script->lua, 1); atomic_fetch_add_explicit(&script->errors, 1, memory_order_relaxed); set_last_error(script, "event hook is not a function"); return; } push_event(script->lua, event); if (lua_pcall(script->lua, 1, 0, 0) != LUA_OK) { atomic_fetch_add_explicit(&script->errors, 1, memory_order_relaxed); set_last_error(script, lua_tostring(script->lua, -1)); lua_pop(script->lua, 1); return; } atomic_fetch_add_explicit(&script->handled, 1, memory_order_relaxed); } static bool pop_job(naut_script *script, script_job *job) { pthread_mutex_lock(&script->lock); while (!script->stopping && script->count == 0) pthread_cond_wait(&script->ready, &script->lock); if (script->count == 0) { pthread_mutex_unlock(&script->lock); return false; } *job = script->queue[script->head]; if (job->event.message) job->event.message = job->message; if (job->event.path) job->event.path = job->path; script->head = (script->head + 1) % script->capacity; script->count--; pthread_mutex_unlock(&script->lock); return true; } static void *script_worker(void *opaque) { naut_script *script = opaque; script_job job; while (pop_job(script, &job)) run_hook(script, &job.event); return NULL; } static void queue_event(void *opaque, const naut_event *event) { naut_script *script = opaque; pthread_mutex_lock(&script->lock); if (script->stopping || script->count == script->capacity) { pthread_mutex_unlock(&script->lock); atomic_fetch_add_explicit(&script->dropped, 1, memory_order_relaxed); return; } size_t tail = (script->head + script->count) % script->capacity; script_job *job = &script->queue[tail]; memset(job, 0, sizeof(*job)); job->event = *event; if (event->message) { snprintf(job->message, sizeof job->message, "%s", event->message); job->event.message = job->message; } if (event->path) { snprintf(job->path, sizeof job->path, "%s", event->path); job->event.path = job->path; } script->count++; pthread_cond_signal(&script->ready); pthread_mutex_unlock(&script->lock); atomic_fetch_add_explicit(&script->queued, 1, memory_order_relaxed); } naut_script *naut_script_create(naut_event_bus *events, const char *script_path, size_t queue_capacity, naut_script_move_file_cb move_file, void *move_context, naut_err *error) { if (error) *error = NAUT_ERR_INVAL; if (!events || !script_path || !*script_path || queue_capacity == 0) return NULL; naut_script *script = calloc(1, sizeof(*script)); if (!script) { if (error) *error = NAUT_ERR_NOMEM; return NULL; } script->events = events; script->capacity = queue_capacity; script->move_file = move_file; script->move_context = move_context; script->queue = calloc(queue_capacity, sizeof(*script->queue)); if (!script->queue) { if (error) *error = NAUT_ERR_NOMEM; free(script); return NULL; } if (pthread_mutex_init(&script->lock, NULL) != 0) { if (error) *error = NAUT_ERR_NOMEM; free(script->queue); free(script); return NULL; } if (pthread_cond_init(&script->ready, NULL) != 0) { if (error) *error = NAUT_ERR_NOMEM; pthread_mutex_destroy(&script->lock); free(script->queue); free(script); return NULL; } script->lua = luaL_newstate(); if (!script->lua) goto fail; luaL_openlibs(script->lua); sandbox(script->lua); install_api(script); if (luaL_loadfile(script->lua, script_path) != LUA_OK || lua_pcall(script->lua, 0, 0, 0) != LUA_OK) { set_last_error(script, lua_tostring(script->lua, -1)); if (error) *error = NAUT_ERR_PROTO; goto fail; } if (naut_event_subscribe(events, queue_event, script, &script->subscription) != NAUT_OK) goto fail; if (pthread_create(&script->thread, NULL, script_worker, script) != 0) { naut_event_unsubscribe(events, script->subscription); script->subscription = 0; goto fail; } if (error) *error = NAUT_OK; return script; fail: if (error && *error == NAUT_ERR_INVAL) *error = NAUT_ERR_NOMEM; if (script->lua) lua_close(script->lua); pthread_cond_destroy(&script->ready); pthread_mutex_destroy(&script->lock); free(script->queue); free(script); return NULL; } void naut_script_destroy(naut_script *script) { if (!script) return; naut_event_unsubscribe(script->events, script->subscription); pthread_mutex_lock(&script->lock); script->stopping = true; pthread_cond_broadcast(&script->ready); pthread_mutex_unlock(&script->lock); pthread_join(script->thread, NULL); lua_close(script->lua); pthread_cond_destroy(&script->ready); pthread_mutex_destroy(&script->lock); free(script->queue); free(script); } void naut_script_get_stats(const naut_script *script, naut_script_stats *stats) { if (!script || !stats) return; *stats = (naut_script_stats) { .queued = atomic_load_explicit(&script->queued, memory_order_relaxed), .handled = atomic_load_explicit(&script->handled, memory_order_relaxed), .dropped = atomic_load_explicit(&script->dropped, memory_order_relaxed), .errors = atomic_load_explicit(&script->errors, memory_order_relaxed), .move_requests = atomic_load_explicit(&script->move_requests, memory_order_relaxed), }; } const char *naut_script_last_error(naut_script *script) { if (!script) return ""; pthread_mutex_lock(&script->lock); static _Thread_local char copy[256]; snprintf(copy, sizeof copy, "%s", script->last_error); pthread_mutex_unlock(&script->lock); return copy; }