optimize sandbox
This commit is contained in:
parent
0abf31ed3a
commit
ba9a3db0a0
|
@ -1,138 +0,0 @@
|
||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Moonshark/core/runner"
|
|
||||||
"Moonshark/core/utils"
|
|
||||||
"Moonshark/core/utils/logger"
|
|
||||||
"crypto/subtle"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Error for CSRF validation failure
|
|
||||||
var ErrCSRFValidationFailed = errors.New("CSRF token validation failed")
|
|
||||||
|
|
||||||
// ValidateCSRFToken checks if the CSRF token is valid for a request
|
|
||||||
func ValidateCSRFToken(ctx *runner.Context) bool {
|
|
||||||
// Only validate for form submissions
|
|
||||||
method, ok := ctx.Get("method").(string)
|
|
||||||
if !ok || (method != "POST" && method != "PUT" && method != "PATCH" && method != "DELETE") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get form data
|
|
||||||
formData, ok := ctx.Get("form").(map[string]any)
|
|
||||||
if !ok || formData == nil {
|
|
||||||
logger.Warning("CSRF validation failed: no form data")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get token from form
|
|
||||||
formToken, ok := formData["csrf"].(string)
|
|
||||||
if !ok || formToken == "" {
|
|
||||||
logger.Warning("CSRF validation failed: no token in form")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get session from context
|
|
||||||
sessionMap, ok := ctx.Get("session").(map[string]any)
|
|
||||||
if !ok || sessionMap == nil {
|
|
||||||
logger.Warning("CSRF validation failed: no session data")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get session data
|
|
||||||
sessionData, ok := sessionMap["data"].(map[string]any)
|
|
||||||
if !ok || sessionData == nil {
|
|
||||||
logger.Warning("CSRF validation failed: no session data map")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get token from session
|
|
||||||
sessionToken, ok := sessionData["_csrf_token"].(string)
|
|
||||||
if !ok || sessionToken == "" {
|
|
||||||
logger.Warning("CSRF validation failed: no token in session")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constant-time comparison to prevent timing attacks
|
|
||||||
return subtle.ConstantTimeCompare([]byte(formToken), []byte(sessionToken)) == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleCSRFError handles a CSRF validation error
|
|
||||||
func HandleCSRFError(ctx *fasthttp.RequestCtx, errorConfig utils.ErrorPageConfig) {
|
|
||||||
method := string(ctx.Method())
|
|
||||||
path := string(ctx.Path())
|
|
||||||
|
|
||||||
logger.Warning("CSRF validation failed for %s %s", method, path)
|
|
||||||
|
|
||||||
ctx.SetContentType("text/html; charset=utf-8")
|
|
||||||
ctx.SetStatusCode(fasthttp.StatusForbidden)
|
|
||||||
|
|
||||||
errorMsg := "Invalid or missing CSRF token. This could be due to an expired form or a cross-site request forgery attempt."
|
|
||||||
errorHTML := utils.ForbiddenPage(errorConfig, path, errorMsg)
|
|
||||||
ctx.SetBody([]byte(errorHTML))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateCSRFToken creates a new CSRF token and stores it in the session
|
|
||||||
func GenerateCSRFToken(ctx *runner.Context, length int) (string, error) {
|
|
||||||
if length < 16 {
|
|
||||||
length = 16 // Minimum token length for security
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create secure random token
|
|
||||||
token, err := GenerateSecureToken(length)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get session from context
|
|
||||||
sessionMap, ok := ctx.Get("session").(map[string]any)
|
|
||||||
if !ok || sessionMap == nil {
|
|
||||||
return "", errors.New("no session found in context")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get session data
|
|
||||||
sessionData, ok := sessionMap["data"].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
// Initialize session data if it doesn't exist
|
|
||||||
sessionData = make(map[string]any)
|
|
||||||
sessionMap["data"] = sessionData
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store token in session
|
|
||||||
sessionData["_csrf_token"] = token
|
|
||||||
return token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCSRFToken retrieves the current CSRF token or generates a new one
|
|
||||||
func GetCSRFToken(ctx *runner.Context) (string, error) {
|
|
||||||
// Get session from context
|
|
||||||
sessionMap, ok := ctx.Get("session").(map[string]any)
|
|
||||||
if !ok || sessionMap == nil {
|
|
||||||
return "", errors.New("no session found in context")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get session data
|
|
||||||
sessionData, ok := sessionMap["data"].(map[string]any)
|
|
||||||
if !ok || sessionData == nil {
|
|
||||||
return GenerateCSRFToken(ctx, 32)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if token already exists in session
|
|
||||||
if token, ok := sessionData["_csrf_token"].(string); ok && token != "" {
|
|
||||||
return token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate new token
|
|
||||||
return GenerateCSRFToken(ctx, 32)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CSRFMiddleware validates CSRF tokens for state-changing requests
|
|
||||||
func CSRFMiddleware(ctx *runner.Context) error {
|
|
||||||
if !ValidateCSRFToken(ctx) {
|
|
||||||
return ErrCSRFValidationFailed
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -2,7 +2,6 @@ package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"Moonshark/core/metadata"
|
"Moonshark/core/metadata"
|
||||||
|
@ -167,14 +166,6 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
|
||||||
luaCtx.Set("path", path)
|
luaCtx.Set("path", path)
|
||||||
luaCtx.Set("host", host)
|
luaCtx.Set("host", host)
|
||||||
|
|
||||||
// Initialize session
|
|
||||||
session := s.sessionManager.GetSessionFromRequest(ctx)
|
|
||||||
sessionMap := map[string]any{
|
|
||||||
"id": session.ID,
|
|
||||||
"data": session.Data,
|
|
||||||
}
|
|
||||||
luaCtx.Set("session", sessionMap)
|
|
||||||
|
|
||||||
// URL parameters
|
// URL parameters
|
||||||
if params.Count > 0 {
|
if params.Count > 0 {
|
||||||
paramMap := make(map[string]any, params.Count)
|
paramMap := make(map[string]any, params.Count)
|
||||||
|
@ -201,25 +192,11 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
|
||||||
luaCtx.Set("form", make(map[string]any))
|
luaCtx.Set("form", make(map[string]any))
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSRF middleware for state-changing requests
|
|
||||||
if method == "POST" || method == "PUT" || method == "PATCH" || method == "DELETE" {
|
|
||||||
if !ValidateCSRFToken(luaCtx) {
|
|
||||||
HandleCSRFError(ctx, s.errorConfig)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute Lua script
|
// Execute Lua script
|
||||||
response, err := s.luaRunner.Run(bytecode, luaCtx, scriptPath)
|
response, err := s.luaRunner.Run(bytecode, luaCtx, scriptPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Error executing Lua route: %v", err)
|
logger.Error("Error executing Lua route: %v", err)
|
||||||
|
|
||||||
// Special handling for specific errors
|
|
||||||
if errors.Is(err, ErrCSRFValidationFailed) {
|
|
||||||
HandleCSRFError(ctx, s.errorConfig)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// General error handling
|
// General error handling
|
||||||
ctx.SetContentType("text/html; charset=utf-8")
|
ctx.SetContentType("text/html; charset=utf-8")
|
||||||
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
|
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
|
||||||
|
@ -228,15 +205,6 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update session if modified
|
|
||||||
if response.SessionModified {
|
|
||||||
for k, v := range response.SessionData {
|
|
||||||
session.Set(k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.sessionManager.ApplySessionCookie(ctx, session)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply response to HTTP context
|
// Apply response to HTTP context
|
||||||
runner.ApplyResponse(response, ctx)
|
runner.ApplyResponse(response, ctx)
|
||||||
|
|
||||||
|
|
|
@ -115,19 +115,7 @@ func (s *Sandbox) Execute(state *luajit.State, bytecode []byte, ctx *Context) (*
|
||||||
// Create a response object
|
// Create a response object
|
||||||
response := NewResponse()
|
response := NewResponse()
|
||||||
|
|
||||||
// Load bytecode
|
// Get the execution function first
|
||||||
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")
|
state.GetGlobal("__execute_script")
|
||||||
if !state.IsFunction(-1) {
|
if !state.IsFunction(-1) {
|
||||||
state.Pop(1)
|
state.Pop(1)
|
||||||
|
@ -135,11 +123,19 @@ func (s *Sandbox) Execute(state *luajit.State, bytecode []byte, ctx *Context) (*
|
||||||
return nil, ErrSandboxNotInitialized
|
return nil, ErrSandboxNotInitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push function and bytecode
|
// Load bytecode
|
||||||
state.PushCopy(-2) // Bytecode
|
if err := state.LoadBytecode(bytecode, "script"); err != nil {
|
||||||
state.PushCopy(-2) // Context
|
state.Pop(1) // Pop the __execute_script function
|
||||||
state.Remove(-4) // Remove bytecode duplicate
|
ReleaseResponse(response)
|
||||||
state.Remove(-3) // Remove context duplicate
|
return nil, fmt.Errorf("failed to load script: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push context values
|
||||||
|
if err := state.PushTable(ctx.Values); err != nil {
|
||||||
|
state.Pop(2) // Pop bytecode and __execute_script
|
||||||
|
ReleaseResponse(response)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Execute with 2 args, 1 result
|
// Execute with 2 args, 1 result
|
||||||
if err := state.Call(2, 1); err != nil {
|
if err := state.Call(2, 1); err != nil {
|
||||||
|
@ -222,32 +218,8 @@ func extractHTTPResponseData(state *luajit.State, response *Response) {
|
||||||
}
|
}
|
||||||
state.Pop(1)
|
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
|
// Clean up
|
||||||
state.Pop(1)
|
state.Pop(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractCookie pulls cookie data from the current table on the stack
|
// extractCookie pulls cookie data from the current table on the stack
|
||||||
|
|
|
@ -6,14 +6,10 @@ including core modules and utilities. It's designed to be embedded in the
|
||||||
Go binary at build time.
|
Go binary at build time.
|
||||||
]]--
|
]]--
|
||||||
|
|
||||||
-- Global tables for execution context
|
__http_response = {}
|
||||||
__http_responses = {}
|
|
||||||
__module_paths = {}
|
__module_paths = {}
|
||||||
__module_bytecode = {}
|
__module_bytecode = {}
|
||||||
__ready_modules = {}
|
__ready_modules = {}
|
||||||
__session_data = {}
|
|
||||||
__session_id = nil
|
|
||||||
__session_modified = false
|
|
||||||
|
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
-- CORE SANDBOX FUNCTIONALITY
|
-- CORE SANDBOX FUNCTIONALITY
|
||||||
|
@ -21,15 +17,12 @@ __session_modified = false
|
||||||
|
|
||||||
-- Create environment inheriting from _G
|
-- Create environment inheriting from _G
|
||||||
function __create_env(ctx)
|
function __create_env(ctx)
|
||||||
-- Create environment with metatable inheriting from _G
|
|
||||||
local env = setmetatable({}, {__index = _G})
|
local env = setmetatable({}, {__index = _G})
|
||||||
|
|
||||||
-- Add context if provided
|
|
||||||
if ctx then
|
if ctx then
|
||||||
env.ctx = ctx
|
env.ctx = ctx
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Add proper require function to this environment
|
|
||||||
if __setup_require then
|
if __setup_require then
|
||||||
__setup_require(env)
|
__setup_require(env)
|
||||||
end
|
end
|
||||||
|
@ -39,188 +32,163 @@ end
|
||||||
|
|
||||||
-- Execute script with clean environment
|
-- Execute script with clean environment
|
||||||
function __execute_script(fn, ctx)
|
function __execute_script(fn, ctx)
|
||||||
-- Clear previous responses
|
__http_response = nil
|
||||||
__http_responses[1] = nil
|
|
||||||
|
|
||||||
-- Create environment with metatable inheriting from _G
|
local env = __create_env(ctx)
|
||||||
local env = setmetatable({}, {__index = _G})
|
|
||||||
|
|
||||||
-- Add context if provided
|
|
||||||
if ctx then
|
|
||||||
env.ctx = ctx
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Initialize local session variables in the environment
|
|
||||||
local sessionData = {}
|
|
||||||
local sessionId = ""
|
|
||||||
|
|
||||||
if ctx.session then
|
|
||||||
sessionId = ctx.session.id or ""
|
|
||||||
sessionData = ctx.session.data or {}
|
|
||||||
end
|
|
||||||
|
|
||||||
env.__session_data = sessionData
|
|
||||||
env.__session_id = sessionId
|
|
||||||
env.__session_modified = false
|
|
||||||
|
|
||||||
-- Set environment for function
|
|
||||||
setfenv(fn, env)
|
setfenv(fn, env)
|
||||||
|
|
||||||
-- Execute with protected call
|
|
||||||
local ok, result = pcall(fn)
|
local ok, result = pcall(fn)
|
||||||
if not ok then
|
if not ok then
|
||||||
error(result, 0)
|
error(result, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- If session was modified, add to response
|
|
||||||
if env.__session_modified then
|
|
||||||
__http_responses[1] = __http_responses[1] or {}
|
|
||||||
__http_responses[1].session_data = env.__session_data
|
|
||||||
__http_responses[1].session_modified = true
|
|
||||||
end
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Ensure __http_response exists, then return it
|
||||||
|
function __ensure_response()
|
||||||
|
if not __http_response then
|
||||||
|
__http_response = {}
|
||||||
|
end
|
||||||
|
return __http_response
|
||||||
|
end
|
||||||
|
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
-- HTTP MODULE
|
-- HTTP MODULE
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
|
|
||||||
-- HTTP module implementation
|
-- HTTP module implementation
|
||||||
local http = {
|
local http = {
|
||||||
-- Set HTTP status code
|
-- Set HTTP status code
|
||||||
set_status = function(code)
|
set_status = function(code)
|
||||||
if type(code) ~= "number" then
|
if type(code) ~= "number" then
|
||||||
error("http.set_status: status code must be a number", 2)
|
error("http.set_status: status code must be a number", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
local resp = __http_responses[1] or {}
|
local resp = __ensure_response()
|
||||||
resp.status = code
|
resp.status = code
|
||||||
__http_responses[1] = resp
|
end,
|
||||||
end,
|
|
||||||
|
|
||||||
-- Set HTTP header
|
-- Set HTTP header
|
||||||
set_header = function(name, value)
|
set_header = function(name, value)
|
||||||
if type(name) ~= "string" or type(value) ~= "string" then
|
if type(name) ~= "string" or type(value) ~= "string" then
|
||||||
error("http.set_header: name and value must be strings", 2)
|
error("http.set_header: name and value must be strings", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
local resp = __http_responses[1] or {}
|
local resp = __ensure_response()
|
||||||
resp.headers = resp.headers or {}
|
resp.headers = resp.headers or {}
|
||||||
resp.headers[name] = value
|
resp.headers[name] = value
|
||||||
__http_responses[1] = resp
|
end,
|
||||||
end,
|
|
||||||
|
|
||||||
-- Set content type; set_header helper
|
-- Set content type; set_header helper
|
||||||
set_content_type = function(content_type)
|
set_content_type = function(content_type)
|
||||||
http.set_header("Content-Type", content_type)
|
http.set_header("Content-Type", content_type)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
-- Set metadata (arbitrary data to be returned with response)
|
-- Set metadata (arbitrary data to be returned with response)
|
||||||
set_metadata = function(key, value)
|
set_metadata = function(key, value)
|
||||||
if type(key) ~= "string" then
|
if type(key) ~= "string" then
|
||||||
error("http.set_metadata: key must be a string", 2)
|
error("http.set_metadata: key must be a string", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
local resp = __http_responses[1] or {}
|
local resp = __ensure_response()
|
||||||
resp.metadata = resp.metadata or {}
|
resp.metadata = resp.metadata or {}
|
||||||
resp.metadata[key] = value
|
resp.metadata[key] = value
|
||||||
__http_responses[1] = resp
|
end,
|
||||||
end,
|
|
||||||
|
|
||||||
-- HTTP client submodule
|
-- HTTP client submodule
|
||||||
client = {
|
client = {
|
||||||
-- Generic request function
|
-- Generic request function
|
||||||
request = function(method, url, body, options)
|
request = function(method, url, body, options)
|
||||||
if type(method) ~= "string" then
|
if type(method) ~= "string" then
|
||||||
error("http.client.request: method must be a string", 2)
|
error("http.client.request: method must be a string", 2)
|
||||||
end
|
end
|
||||||
if type(url) ~= "string" then
|
if type(url) ~= "string" then
|
||||||
error("http.client.request: url must be a string", 2)
|
error("http.client.request: url must be a string", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Call native implementation
|
-- Call native implementation
|
||||||
local result = __http_request(method, url, body, options)
|
local result = __http_request(method, url, body, options)
|
||||||
return result
|
return result
|
||||||
end,
|
end,
|
||||||
|
|
||||||
-- Simple GET request
|
-- Simple GET request
|
||||||
get = function(url, options)
|
get = function(url, options)
|
||||||
return http.client.request("GET", url, nil, options)
|
return http.client.request("GET", url, nil, options)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
-- Simple POST request with automatic content-type
|
-- Simple POST request with automatic content-type
|
||||||
post = function(url, body, options)
|
post = function(url, body, options)
|
||||||
options = options or {}
|
options = options or {}
|
||||||
return http.client.request("POST", url, body, options)
|
return http.client.request("POST", url, body, options)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
-- Simple PUT request with automatic content-type
|
-- Simple PUT request with automatic content-type
|
||||||
put = function(url, body, options)
|
put = function(url, body, options)
|
||||||
options = options or {}
|
options = options or {}
|
||||||
return http.client.request("PUT", url, body, options)
|
return http.client.request("PUT", url, body, options)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
-- Simple DELETE request
|
-- Simple DELETE request
|
||||||
delete = function(url, options)
|
delete = function(url, options)
|
||||||
return http.client.request("DELETE", url, nil, options)
|
return http.client.request("DELETE", url, nil, options)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
-- Simple PATCH request
|
-- Simple PATCH request
|
||||||
patch = function(url, body, options)
|
patch = function(url, body, options)
|
||||||
options = options or {}
|
options = options or {}
|
||||||
return http.client.request("PATCH", url, body, options)
|
return http.client.request("PATCH", url, body, options)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
-- Simple HEAD request
|
-- Simple HEAD request
|
||||||
head = function(url, options)
|
head = function(url, options)
|
||||||
options = options or {}
|
options = options or {}
|
||||||
return http.client.request("HEAD", url, nil, options)
|
return http.client.request("HEAD", url, nil, options)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
-- Simple OPTIONS request
|
-- Simple OPTIONS request
|
||||||
options = function(url, options)
|
options = function(url, options)
|
||||||
return http.client.request("OPTIONS", url, nil, options)
|
return http.client.request("OPTIONS", url, nil, options)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
-- Shorthand function to directly get JSON
|
-- Shorthand function to directly get JSON
|
||||||
get_json = function(url, options)
|
get_json = function(url, options)
|
||||||
options = options or {}
|
options = options or {}
|
||||||
local response = http.client.get(url, options)
|
local response = http.client.get(url, options)
|
||||||
if response.ok and response.json then
|
if response.ok and response.json then
|
||||||
return response.json
|
return response.json
|
||||||
end
|
end
|
||||||
return nil, response
|
return nil, response
|
||||||
end,
|
end,
|
||||||
|
|
||||||
-- Utility to build a URL with query parameters
|
-- Utility to build a URL with query parameters
|
||||||
build_url = function(base_url, params)
|
build_url = function(base_url, params)
|
||||||
if not params or type(params) ~= "table" then
|
if not params or type(params) ~= "table" then
|
||||||
return base_url
|
return base_url
|
||||||
end
|
end
|
||||||
|
|
||||||
local query = {}
|
local query = {}
|
||||||
for k, v in pairs(params) do
|
for k, v in pairs(params) do
|
||||||
if type(v) == "table" then
|
if type(v) == "table" then
|
||||||
for _, item in ipairs(v) do
|
for _, item in ipairs(v) do
|
||||||
table.insert(query, k .. "=" .. tostring(item))
|
table.insert(query, k .. "=" .. tostring(item))
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
table.insert(query, k .. "=" .. tostring(v))
|
table.insert(query, k .. "=" .. tostring(v))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if #query > 0 then
|
if #query > 0 then
|
||||||
if base_url:find("?") then
|
if base_url:find("?") then
|
||||||
return base_url .. "&" .. table.concat(query, "&")
|
return base_url .. "&" .. table.concat(query, "&")
|
||||||
else
|
else
|
||||||
return base_url .. "?" .. table.concat(query, "&")
|
return base_url .. "?" .. table.concat(query, "&")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return base_url
|
return base_url
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
|
@ -235,15 +203,10 @@ local cookie = {
|
||||||
error("cookie.set: name must be a string", 2)
|
error("cookie.set: name must be a string", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Get or create response
|
local resp = __ensure_response()
|
||||||
local resp = __http_responses[1] or {}
|
|
||||||
resp.cookies = resp.cookies or {}
|
resp.cookies = resp.cookies or {}
|
||||||
__http_responses[1] = resp
|
|
||||||
|
|
||||||
-- Handle options as table
|
|
||||||
local opts = options or {}
|
local opts = options or {}
|
||||||
|
|
||||||
-- Create cookie table
|
|
||||||
local cookie = {
|
local cookie = {
|
||||||
name = name,
|
name = name,
|
||||||
value = value or "",
|
value = value or "",
|
||||||
|
@ -251,7 +214,6 @@ local cookie = {
|
||||||
domain = opts.domain
|
domain = opts.domain
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Handle expiry
|
|
||||||
if opts.expires then
|
if opts.expires then
|
||||||
if type(opts.expires) == "number" then
|
if type(opts.expires) == "number" then
|
||||||
if opts.expires > 0 then
|
if opts.expires > 0 then
|
||||||
|
@ -266,13 +228,10 @@ local cookie = {
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Security flags
|
|
||||||
cookie.secure = (opts.secure ~= false)
|
cookie.secure = (opts.secure ~= false)
|
||||||
cookie.http_only = (opts.http_only ~= false)
|
cookie.http_only = (opts.http_only ~= false)
|
||||||
|
|
||||||
-- Store in cookies table
|
table.insert(resp.cookies, cookie)
|
||||||
local n = #resp.cookies + 1
|
|
||||||
resp.cookies[n] = cookie
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
end,
|
end,
|
||||||
|
@ -283,15 +242,12 @@ local cookie = {
|
||||||
error("cookie.get: name must be a string", 2)
|
error("cookie.get: name must be a string", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Access values directly from current environment
|
|
||||||
local env = getfenv(2)
|
local env = getfenv(2)
|
||||||
|
|
||||||
-- Check if context exists and has cookies
|
|
||||||
if env.ctx and env.ctx.cookies then
|
if env.ctx and env.ctx.cookies then
|
||||||
return env.ctx.cookies[name]
|
return env.ctx.cookies[name]
|
||||||
end
|
end
|
||||||
|
|
||||||
-- If context has request_cookies map
|
|
||||||
if env.ctx and env.ctx._request_cookies then
|
if env.ctx and env.ctx._request_cookies then
|
||||||
return env.ctx._request_cookies[name]
|
return env.ctx._request_cookies[name]
|
||||||
end
|
end
|
||||||
|
@ -305,185 +261,10 @@ local cookie = {
|
||||||
error("cookie.remove: name must be a string", 2)
|
error("cookie.remove: name must be a string", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Create an expired cookie
|
|
||||||
return cookie.set(name, "", {expires = 0, path = path or "/", domain = domain})
|
return cookie.set(name, "", {expires = 0, path = path or "/", domain = domain})
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
-- ======================================================================
|
|
||||||
-- SESSION MODULE
|
|
||||||
-- ======================================================================
|
|
||||||
|
|
||||||
local session = {
|
|
||||||
-- Get session value
|
|
||||||
get = function(key)
|
|
||||||
if type(key) ~= "string" then
|
|
||||||
error("session.get: key must be a string", 2)
|
|
||||||
end
|
|
||||||
local env = getfenv(2)
|
|
||||||
return env.__session_data and env.__session_data[key]
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- Set session value
|
|
||||||
set = function(key, value)
|
|
||||||
if type(key) ~= "string" then
|
|
||||||
error("session.set: key must be a string", 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
local env = getfenv(2)
|
|
||||||
print("SET ENV:", tostring(env)) -- Debug the environment
|
|
||||||
|
|
||||||
if not env.__session_data then
|
|
||||||
env.__session_data = {}
|
|
||||||
print("CREATED NEW SESSION TABLE")
|
|
||||||
end
|
|
||||||
|
|
||||||
env.__session_data[key] = value
|
|
||||||
env.__session_modified = true
|
|
||||||
print("SET:", key, "=", tostring(value), "MODIFIED:", env.__session_modified)
|
|
||||||
return true
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- Delete session value
|
|
||||||
delete = function(key)
|
|
||||||
if type(key) ~= "string" then
|
|
||||||
error("session.delete: key must be a string", 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
local env = getfenv(2)
|
|
||||||
if env.__session_data and env.__session_data[key] ~= nil then
|
|
||||||
env.__session_data[key] = nil
|
|
||||||
env.__session_modified = true
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- Clear all session data
|
|
||||||
clear = function()
|
|
||||||
local env = getfenv(2)
|
|
||||||
if env.__session_data and next(env.__session_data) then
|
|
||||||
env.__session_data = {}
|
|
||||||
env.__session_modified = true
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- Get session ID
|
|
||||||
get_id = function()
|
|
||||||
local env = getfenv(2)
|
|
||||||
return env.__session_id or ""
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- Get all session data
|
|
||||||
get_all = function()
|
|
||||||
local env = getfenv(2)
|
|
||||||
return env.__session_data or {}
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- Check if session has key
|
|
||||||
has = function(key)
|
|
||||||
if type(key) ~= "string" then
|
|
||||||
error("session.has: key must be a string", 2)
|
|
||||||
end
|
|
||||||
local env = getfenv(2)
|
|
||||||
return env.__session_data ~= nil and env.__session_data[key] ~= nil
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
-- ======================================================================
|
|
||||||
-- CSRF MODULE
|
|
||||||
-- ======================================================================
|
|
||||||
|
|
||||||
-- CSRF protection module
|
|
||||||
local csrf = {
|
|
||||||
-- Session key where the token is stored
|
|
||||||
TOKEN_KEY = "_csrf_token",
|
|
||||||
|
|
||||||
-- Default form field name
|
|
||||||
DEFAULT_FIELD = "csrf",
|
|
||||||
|
|
||||||
-- Generate a new CSRF token and store it in the session
|
|
||||||
generate = function(length)
|
|
||||||
-- Default length is 32 characters
|
|
||||||
length = length or 32
|
|
||||||
|
|
||||||
if length < 16 then
|
|
||||||
-- Enforce minimum security
|
|
||||||
length = 16
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Check if we have a session module
|
|
||||||
if not session then
|
|
||||||
error("CSRF protection requires the session module", 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
local token = __generate_token(length)
|
|
||||||
session.set(csrf.TOKEN_KEY, token)
|
|
||||||
return token
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- Get the current token or generate a new one
|
|
||||||
token = function()
|
|
||||||
-- Get from session if exists
|
|
||||||
local token = session.get(csrf.TOKEN_KEY)
|
|
||||||
|
|
||||||
-- Generate if needed
|
|
||||||
if not token then
|
|
||||||
token = csrf.generate()
|
|
||||||
end
|
|
||||||
|
|
||||||
return token
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- Generate a hidden form field with the CSRF token
|
|
||||||
field = function(field_name)
|
|
||||||
field_name = field_name or csrf.DEFAULT_FIELD
|
|
||||||
local token = csrf.token()
|
|
||||||
return string.format('<input type="hidden" name="%s" value="%s">', field_name, token)
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- Verify a given token against the session token
|
|
||||||
verify = function(token, field_name)
|
|
||||||
field_name = field_name or csrf.DEFAULT_FIELD
|
|
||||||
|
|
||||||
local env = getfenv(2)
|
|
||||||
|
|
||||||
local form = nil
|
|
||||||
if env.ctx and env.ctx._request_form then
|
|
||||||
form = env.ctx._request_form
|
|
||||||
elseif env.ctx and env.ctx.form then
|
|
||||||
form = env.ctx.form
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
token = token or form[field_name]
|
|
||||||
if not token then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local session_token = session.get(csrf.TOKEN_KEY)
|
|
||||||
if not session_token then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Constant-time comparison to prevent timing attacks
|
|
||||||
if #token ~= #session_token then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local result = true
|
|
||||||
for i = 1, #token do
|
|
||||||
if token:sub(i, i) ~= session_token:sub(i, i) then
|
|
||||||
result = false
|
|
||||||
-- Don't break early - continue to prevent timing attacks
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
-- UTIL MODULE
|
-- UTIL MODULE
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
|
@ -575,6 +356,4 @@ local util = {
|
||||||
-- Install modules in global scope
|
-- Install modules in global scope
|
||||||
_G.http = http
|
_G.http = http
|
||||||
_G.cookie = cookie
|
_G.cookie = cookie
|
||||||
_G.session = session
|
|
||||||
_G.csrf = csrf
|
|
||||||
_G.util = util
|
_G.util = util
|
||||||
|
|
Loading…
Reference in New Issue
Block a user