Moonshark/core/runner/http.go
2025-03-26 12:18:15 -05:00

193 lines
4.5 KiB
Go

package runner
import (
"net/http"
"sync"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
)
// HTTPResponse represents an HTTP response from Lua
type HTTPResponse struct {
Status int `json:"status"`
Headers map[string]string `json:"headers"`
Body any `json:"body"`
Cookies []*http.Cookie `json:"-"`
}
// Response pool to reduce allocations
var responsePool = sync.Pool{
New: func() interface{} {
return &HTTPResponse{
Status: 200,
Headers: make(map[string]string, 8), // Pre-allocate with reasonable capacity
Cookies: make([]*http.Cookie, 0, 4), // Pre-allocate with reasonable capacity
}
},
}
// NewHTTPResponse creates a default HTTP response, potentially reusing one from the pool
func NewHTTPResponse() *HTTPResponse {
return responsePool.Get().(*HTTPResponse)
}
// ReleaseResponse returns the response to the pool after clearing its values
func ReleaseResponse(resp *HTTPResponse) {
if resp == nil {
return
}
// Clear all values to prevent data leakage
resp.Status = 200 // Reset to default
// Clear headers
for k := range resp.Headers {
delete(resp.Headers, k)
}
// Clear cookies
resp.Cookies = resp.Cookies[:0] // Keep capacity but set length to 0
// Clear body
resp.Body = nil
responsePool.Put(resp)
}
// LuaHTTPModule is the pure Lua implementation of the HTTP module
const LuaHTTPModule = `
-- Table to store response data
__http_responses = {}
-- HTTP module implementation
local http = {
-- Set HTTP status code
set_status = function(code)
if type(code) ~= "number" then
error("http.set_status: status code must be a number", 2)
end
local resp = __http_responses[1] or {}
resp.status = code
__http_responses[1] = resp
end,
-- Set HTTP header
set_header = function(name, value)
if type(name) ~= "string" or type(value) ~= "string" then
error("http.set_header: name and value must be strings", 2)
end
local resp = __http_responses[1] or {}
resp.headers = resp.headers or {}
resp.headers[name] = value
__http_responses[1] = resp
end
}
-- Set content type; set_header helper
http.set_content_type = function(content_type)
http.set_header("Content-Type", content_type)
end
-- Install HTTP module
_G.http = http
-- Override sandbox executor to clear HTTP responses
local old_execute_sandbox = __execute_sandbox
__execute_sandbox = function(bytecode, ctx)
-- Clear previous response for this thread
__http_responses[1] = nil
-- Execute the original function
local result = old_execute_sandbox(bytecode, ctx)
-- Return the result unchanged
return result
end
-- Make sure the HTTP module is accessible in sandbox
if __env_system and __env_system.base_env then
__env_system.base_env.http = http
end
`
// HTTPModuleInitFunc returns an initializer function for the HTTP module
func HTTPModuleInitFunc() StateInitFunc {
return func(state *luajit.State) error {
// Initialize pure Lua HTTP module
return state.DoString(LuaHTTPModule)
}
}
// GetHTTPResponse extracts the HTTP response from Lua state
func GetHTTPResponse(state *luajit.State) (*HTTPResponse, bool) {
response := NewHTTPResponse()
// Get response table
state.GetGlobal("__http_responses")
if state.IsNil(-1) {
state.Pop(1)
ReleaseResponse(response) // Return unused response to pool
return nil, false
}
// Check for response at thread index
state.PushNumber(1)
state.GetTable(-2)
if state.IsNil(-1) {
state.Pop(2)
ReleaseResponse(response) // Return unused response to pool
return nil, false
}
// Get status
state.GetField(-1, "status")
if state.IsNumber(-1) {
response.Status = int(state.ToNumber(-1))
}
state.Pop(1)
// Get headers
state.GetField(-1, "headers")
if state.IsTable(-1) {
// Iterate through headers table
state.PushNil() // Start iteration
for state.Next(-2) {
// Stack has key at -2 and value at -1
if state.IsString(-2) && state.IsString(-1) {
key := state.ToString(-2)
value := state.ToString(-1)
response.Headers[key] = value
}
state.Pop(1) // Pop value, leave key for next iteration
}
}
state.Pop(1)
// Get cookies
state.GetField(-1, "cookies")
if state.IsTable(-1) {
// Iterate through cookies array
length := state.GetTableLength(-1)
for i := 1; i <= length; i++ {
state.PushNumber(float64(i))
state.GetTable(-2)
if state.IsTable(-1) {
cookie := extractCookie(state)
if cookie != nil {
response.Cookies = append(response.Cookies, cookie)
}
}
state.Pop(1)
}
}
state.Pop(1)
// Clean up
state.Pop(2) // Pop response table and __http_responses
return response, true
}