258 lines
6.6 KiB
Go
258 lines
6.6 KiB
Go
package runner
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/goccy/go-json"
|
|
"github.com/valyala/bytebufferpool"
|
|
|
|
"Moonshark/core/logger"
|
|
|
|
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
|
|
)
|
|
|
|
// Sandbox provides a secure execution environment for Lua scripts
|
|
type Sandbox struct {
|
|
modules map[string]any // Custom modules for environment
|
|
debug bool // Enable debug output
|
|
mu sync.RWMutex // Protects modules
|
|
}
|
|
|
|
// NewSandbox creates a new sandbox environment
|
|
func NewSandbox() *Sandbox {
|
|
return &Sandbox{
|
|
modules: make(map[string]any, 8), // Pre-allocate with reasonable capacity
|
|
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...)
|
|
}
|
|
}
|
|
|
|
// debugLog logs a message if debug mode is enabled
|
|
func (s *Sandbox) debugLogCont(format string, args ...interface{}) {
|
|
if s.debug {
|
|
logger.DebugCont(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, stateIndex int) error {
|
|
verbose := stateIndex == 0
|
|
|
|
if verbose {
|
|
s.debugLog("is setting up...")
|
|
}
|
|
|
|
// Register modules in the global environment
|
|
s.mu.RLock()
|
|
for name, module := range s.modules {
|
|
if verbose {
|
|
s.debugLog("is registering module: %s", name)
|
|
}
|
|
if err := state.PushValue(module); err != nil {
|
|
s.mu.RUnlock()
|
|
if verbose {
|
|
s.debugLog("failed to register module %s: %v", name, err)
|
|
}
|
|
return err
|
|
}
|
|
state.SetGlobal(name)
|
|
}
|
|
s.mu.RUnlock()
|
|
|
|
// Initialize environment setup
|
|
err := state.DoString(`
|
|
-- Global tables for response handling
|
|
__http_responses = __http_responses or {}
|
|
|
|
-- Create environment inheriting from _G
|
|
function __create_env(ctx)
|
|
-- Create environment with metatable inheriting from _G
|
|
local env = setmetatable({}, {__index = _G})
|
|
|
|
-- Add context if provided
|
|
if ctx then
|
|
env.ctx = ctx
|
|
end
|
|
|
|
-- Add proper require function to this environment
|
|
if __setup_require then
|
|
__setup_require(env)
|
|
end
|
|
|
|
return env
|
|
end
|
|
|
|
-- Execute script with clean environment
|
|
function __execute_script(fn, ctx)
|
|
-- Clear previous responses
|
|
__http_responses[1] = nil
|
|
|
|
-- Create environment
|
|
local env = __create_env(ctx)
|
|
|
|
-- Set environment for function
|
|
setfenv(fn, env)
|
|
|
|
-- Execute with protected call
|
|
local ok, result = pcall(fn)
|
|
if not ok then
|
|
error(result, 0)
|
|
end
|
|
|
|
return result
|
|
end
|
|
`)
|
|
|
|
if err != nil {
|
|
if verbose {
|
|
s.debugLog("failed to set up...")
|
|
s.debugLogCont("%v", err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
if verbose {
|
|
s.debugLogCont("Complete")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Execute runs bytecode in the sandbox
|
|
func (s *Sandbox) Execute(state *luajit.State, bytecode []byte, ctx map[string]any) (any, error) {
|
|
// Create a temporary context if we only have a map
|
|
if ctx != nil {
|
|
tempCtx := &Context{
|
|
Values: ctx,
|
|
}
|
|
return s.OptimizedExecute(state, bytecode, tempCtx)
|
|
}
|
|
|
|
// Just pass nil through if we have no context
|
|
return s.OptimizedExecute(state, bytecode, nil)
|
|
}
|
|
|
|
// OptimizedExecute runs bytecode with a fasthttp context if available
|
|
func (s *Sandbox) OptimizedExecute(state *luajit.State, bytecode []byte, ctx *Context) (any, error) {
|
|
// Use a buffer from the pool for any string operations
|
|
buf := bytebufferpool.Get()
|
|
defer bytebufferpool.Put(buf)
|
|
|
|
// Load bytecode
|
|
if err := state.LoadBytecode(bytecode, "script"); err != nil {
|
|
s.debugLog("Failed to load bytecode: %v", err)
|
|
return nil, fmt.Errorf("failed to load script: %w", err)
|
|
}
|
|
|
|
// Prepare context values
|
|
var ctxValues map[string]any
|
|
if ctx != nil {
|
|
ctxValues = ctx.Values
|
|
} else {
|
|
ctxValues = nil
|
|
}
|
|
|
|
// Prepare context table
|
|
if ctxValues != nil {
|
|
state.CreateTable(0, len(ctxValues))
|
|
for k, v := range ctxValues {
|
|
state.PushString(k)
|
|
if err := state.PushValue(v); err != nil {
|
|
state.Pop(2) // Pop key and table
|
|
s.debugLog("Failed to push context value %s: %v", k, err)
|
|
return nil, fmt.Errorf("failed to prepare context: %w", err)
|
|
}
|
|
state.SetTable(-3)
|
|
}
|
|
} else {
|
|
state.PushNil() // No context
|
|
}
|
|
|
|
// Get execution function
|
|
state.GetGlobal("__execute_script")
|
|
if !state.IsFunction(-1) {
|
|
state.Pop(2) // Pop context and non-function
|
|
s.debugLog("__execute_script is not a function")
|
|
return nil, fmt.Errorf("sandbox execution function not found")
|
|
}
|
|
|
|
// Stack setup for call: __execute_script, bytecode function, context
|
|
state.PushCopy(-3) // bytecode function (copy from -3)
|
|
state.PushCopy(-3) // context (copy from -3)
|
|
|
|
// Clean up duplicate references
|
|
state.Remove(-5) // Remove original bytecode function
|
|
state.Remove(-4) // Remove original context
|
|
|
|
// Call with 2 args (function, context), 1 result
|
|
if err := state.Call(2, 1); err != nil {
|
|
s.debugLog("Execution failed: %v", err)
|
|
return nil, fmt.Errorf("script execution failed: %w", err)
|
|
}
|
|
|
|
// Get result
|
|
result, err := state.ToValue(-1)
|
|
state.Pop(1) // Pop result
|
|
|
|
// Check for HTTP response
|
|
httpResponse, hasResponse := GetHTTPResponse(state)
|
|
if hasResponse {
|
|
// Add the script result as the response body
|
|
httpResponse.Body = result
|
|
|
|
// If we have a fasthttp context, apply the response directly
|
|
if ctx != nil && ctx.RequestCtx != nil {
|
|
ApplyHTTPResponse(httpResponse, ctx.RequestCtx)
|
|
ReleaseResponse(httpResponse)
|
|
return nil, nil // No need to return response object
|
|
}
|
|
|
|
return httpResponse, nil
|
|
}
|
|
|
|
// If we have a fasthttp context and the result needs to be written directly
|
|
if ctx != nil && ctx.RequestCtx != nil && (result != nil) {
|
|
// For direct HTTP responses
|
|
switch r := result.(type) {
|
|
case string:
|
|
ctx.RequestCtx.SetBodyString(r)
|
|
case []byte:
|
|
ctx.RequestCtx.SetBody(r)
|
|
case map[string]any, []any:
|
|
// JSON response
|
|
ctx.RequestCtx.Response.Header.SetContentType("application/json")
|
|
if err := json.NewEncoder(buf).Encode(r); err == nil {
|
|
ctx.RequestCtx.SetBody(buf.Bytes())
|
|
} else {
|
|
ctx.RequestCtx.SetBodyString(fmt.Sprintf("%v", r))
|
|
}
|
|
default:
|
|
// Default string conversion
|
|
ctx.RequestCtx.SetBodyString(fmt.Sprintf("%v", r))
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
return result, err
|
|
}
|