package runner import ( "fmt" "sync" "github.com/valyala/fasthttp" "Moonshark/core/utils/logger" "maps" luajit "git.sharkk.net/Sky/LuaJIT-to-Go" ) // Error represents a simple error string type Error string func (e Error) Error() string { return string(e) } // Error types var ( ErrSandboxNotInitialized = Error("sandbox not initialized") ) // Sandbox provides a secure execution environment for Lua scripts type Sandbox struct { modules map[string]any debug bool mu sync.RWMutex } // NewSandbox creates a new sandbox environment func NewSandbox() *Sandbox { return &Sandbox{ modules: make(map[string]any, 8), debug: false, } } // EnableDebug turns on debug logging func (s *Sandbox) EnableDebug() { s.debug = true } // debugLog logs a message if debug mode is enabled func (s *Sandbox) debugLog(format string, args ...interface{}) { if s.debug { logger.Debug("Sandbox "+format, args...) } } // AddModule adds a module to the sandbox environment func (s *Sandbox) AddModule(name string, module any) { s.mu.Lock() defer s.mu.Unlock() s.modules[name] = module s.debugLog("Added module: %s", name) } // Setup initializes the sandbox in a Lua state func (s *Sandbox) Setup(state *luajit.State) error { s.debugLog("Setting up sandbox...") // Load the sandbox code if err := loadSandboxIntoState(state); err != nil { s.debugLog("Failed to load sandbox: %v", err) return err } // Register core functions if err := s.registerCoreFunctions(state); err != nil { s.debugLog("Failed to register core functions: %v", err) return err } // Register custom modules in the global environment s.mu.RLock() for name, module := range s.modules { s.debugLog("Registering module: %s", name) if err := state.PushValue(module); err != nil { s.mu.RUnlock() s.debugLog("Failed to register module %s: %v", name, err) return err } state.SetGlobal(name) } s.mu.RUnlock() s.debugLog("Sandbox setup complete") return nil } // registerCoreFunctions registers all built-in functions in the Lua state func (s *Sandbox) registerCoreFunctions(state *luajit.State) error { // Register HTTP functions if err := state.RegisterGoFunction("__http_request", httpRequest); err != nil { return err } // Register utility functions if err := state.RegisterGoFunction("__generate_token", generateToken); err != nil { return err } // Additional registrations can be added here return nil } // Execute runs a Lua script in the sandbox with the given context func (s *Sandbox) Execute(state *luajit.State, bytecode []byte, ctx *Context) (*Response, error) { // Create a response object response := NewResponse() // Load bytecode if err := state.LoadBytecode(bytecode, "script"); err != nil { ReleaseResponse(response) return nil, fmt.Errorf("failed to load script: %w", err) } // Set up context values for execution if err := state.PushTable(ctx.Values); err != nil { ReleaseResponse(response) return nil, err } // Get the execution function state.GetGlobal("__execute_script") if !state.IsFunction(-1) { state.Pop(1) ReleaseResponse(response) return nil, ErrSandboxNotInitialized } // Push function and bytecode state.PushCopy(-2) // Bytecode state.PushCopy(-2) // Context state.Remove(-4) // Remove bytecode duplicate state.Remove(-3) // Remove context duplicate // Execute with 2 args, 1 result if err := state.Call(2, 1); err != nil { ReleaseResponse(response) return nil, fmt.Errorf("script execution failed: %w", err) } // Set response body from result body, err := state.ToValue(-1) if err == nil { response.Body = body } state.Pop(1) extractHTTPResponseData(state, response) return response, nil } // extractResponseData pulls response info from the Lua state func extractHTTPResponseData(state *luajit.State, response *Response) { state.GetGlobal("__http_responses") if !state.IsTable(-1) { state.Pop(1) return } state.PushNumber(1) state.GetTable(-2) if !state.IsTable(-1) { state.Pop(2) return } // Extract status state.GetField(-1, "status") if state.IsNumber(-1) { response.Status = int(state.ToNumber(-1)) } state.Pop(1) // Extract headers state.GetField(-1, "headers") if state.IsTable(-1) { state.PushNil() // Start iteration for state.Next(-2) { if state.IsString(-2) && state.IsString(-1) { key := state.ToString(-2) value := state.ToString(-1) response.Headers[key] = value } state.Pop(1) } } state.Pop(1) // Extract cookies state.GetField(-1, "cookies") if state.IsTable(-1) { length := state.GetTableLength(-1) for i := 1; i <= length; i++ { state.PushNumber(float64(i)) state.GetTable(-2) if state.IsTable(-1) { extractCookie(state, response) } state.Pop(1) } } state.Pop(1) // Extract metadata state.GetField(-1, "metadata") if state.IsTable(-1) { table, err := state.ToTable(-1) if err == nil { maps.Copy(response.Metadata, table) } } state.Pop(1) // Check session modified flag state.GetField(-1, "session_modified") if state.IsBoolean(-1) && state.ToBoolean(-1) { logger.DebugCont("Found session_modified=true") response.SessionModified = true // Get session data (using the new structure) state.Pop(1) // Remove session_modified state.GetField(-1, "session_data") if state.IsTable(-1) { sessionData, err := state.ToTable(-1) if err == nil { for k, v := range sessionData { response.SessionData[k] = v } } } state.Pop(1) } else { logger.DebugCont("session_modified is not set or not true") } state.Pop(1) // Clean up state.Pop(1) } // extractCookie pulls cookie data from the current table on the stack func extractCookie(state *luajit.State, response *Response) { cookie := fasthttp.AcquireCookie() // Get name (required) state.GetField(-1, "name") if !state.IsString(-1) { state.Pop(1) fasthttp.ReleaseCookie(cookie) return } cookie.SetKey(state.ToString(-1)) state.Pop(1) // Get value state.GetField(-1, "value") if state.IsString(-1) { cookie.SetValue(state.ToString(-1)) } state.Pop(1) // Get path state.GetField(-1, "path") if state.IsString(-1) { cookie.SetPath(state.ToString(-1)) } else { cookie.SetPath("/") // Default } state.Pop(1) // Get domain state.GetField(-1, "domain") if state.IsString(-1) { cookie.SetDomain(state.ToString(-1)) } state.Pop(1) // Get other parameters state.GetField(-1, "http_only") if state.IsBoolean(-1) { cookie.SetHTTPOnly(state.ToBoolean(-1)) } state.Pop(1) state.GetField(-1, "secure") if state.IsBoolean(-1) { cookie.SetSecure(state.ToBoolean(-1)) } state.Pop(1) state.GetField(-1, "max_age") if state.IsNumber(-1) { cookie.SetMaxAge(int(state.ToNumber(-1))) } state.Pop(1) response.Cookies = append(response.Cookies, cookie) }