diff --git a/core/http/Csrf.go b/core/http/Csrf.go deleted file mode 100644 index f526793..0000000 --- a/core/http/Csrf.go +++ /dev/null @@ -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 -} diff --git a/core/http/Server.go b/core/http/Server.go index a357b2b..a308b3a 100644 --- a/core/http/Server.go +++ b/core/http/Server.go @@ -2,7 +2,6 @@ package http import ( "context" - "errors" "time" "Moonshark/core/metadata" @@ -167,14 +166,6 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip luaCtx.Set("path", path) 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 if params.Count > 0 { 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)) } - // 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 response, err := s.luaRunner.Run(bytecode, luaCtx, scriptPath) if err != nil { 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 ctx.SetContentType("text/html; charset=utf-8") ctx.SetStatusCode(fasthttp.StatusInternalServerError) @@ -228,15 +205,6 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip 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 runner.ApplyResponse(response, ctx) diff --git a/core/runner/Sandbox.go b/core/runner/Sandbox.go index d6dcf93..c30c71d 100644 --- a/core/runner/Sandbox.go +++ b/core/runner/Sandbox.go @@ -115,19 +115,7 @@ func (s *Sandbox) Execute(state *luajit.State, bytecode []byte, ctx *Context) (* // 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 + // Get the execution function first state.GetGlobal("__execute_script") if !state.IsFunction(-1) { state.Pop(1) @@ -135,11 +123,19 @@ func (s *Sandbox) Execute(state *luajit.State, bytecode []byte, ctx *Context) (* 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 + // Load bytecode + if err := state.LoadBytecode(bytecode, "script"); err != nil { + state.Pop(1) // Pop the __execute_script function + ReleaseResponse(response) + 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 if err := state.Call(2, 1); err != nil { @@ -222,32 +218,8 @@ func extractHTTPResponseData(state *luajit.State, response *Response) { } 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) + state.Pop(2) } // extractCookie pulls cookie data from the current table on the stack diff --git a/core/runner/sandbox.lua b/core/runner/sandbox.lua index c7c6e09..dffcfab 100644 --- a/core/runner/sandbox.lua +++ b/core/runner/sandbox.lua @@ -6,14 +6,10 @@ including core modules and utilities. It's designed to be embedded in the Go binary at build time. ]]-- --- Global tables for execution context -__http_responses = {} +__http_response = {} __module_paths = {} __module_bytecode = {} __ready_modules = {} -__session_data = {} -__session_id = nil -__session_modified = false -- ====================================================================== -- CORE SANDBOX FUNCTIONALITY @@ -21,15 +17,12 @@ __session_modified = false -- 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 @@ -39,188 +32,163 @@ end -- Execute script with clean environment function __execute_script(fn, ctx) - -- Clear previous responses - __http_responses[1] = nil + __http_response = nil - -- Create environment with metatable inheriting from _G - 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 + local env = __create_env(ctx) setfenv(fn, env) - -- Execute with protected call local ok, result = pcall(fn) if not ok then error(result, 0) 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 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 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 + -- 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, + local resp = __ensure_response() + resp.status = code + 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 + -- 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, + local resp = __ensure_response() + resp.headers = resp.headers or {} + resp.headers[name] = value + end, - -- Set content type; set_header helper - set_content_type = function(content_type) - http.set_header("Content-Type", content_type) - end, + -- Set content type; set_header helper + set_content_type = function(content_type) + http.set_header("Content-Type", content_type) + end, - -- Set metadata (arbitrary data to be returned with response) - set_metadata = function(key, value) - if type(key) ~= "string" then - error("http.set_metadata: key must be a string", 2) - end + -- Set metadata (arbitrary data to be returned with response) + set_metadata = function(key, value) + if type(key) ~= "string" then + error("http.set_metadata: key must be a string", 2) + end - local resp = __http_responses[1] or {} - resp.metadata = resp.metadata or {} - resp.metadata[key] = value - __http_responses[1] = resp - end, + local resp = __ensure_response() + resp.metadata = resp.metadata or {} + resp.metadata[key] = value + end, - -- HTTP client submodule - client = { - -- Generic request function - request = function(method, url, body, options) - if type(method) ~= "string" then - error("http.client.request: method must be a string", 2) - end - if type(url) ~= "string" then - error("http.client.request: url must be a string", 2) - end + -- HTTP client submodule + client = { + -- Generic request function + request = function(method, url, body, options) + if type(method) ~= "string" then + error("http.client.request: method must be a string", 2) + end + if type(url) ~= "string" then + error("http.client.request: url must be a string", 2) + end - -- Call native implementation - local result = __http_request(method, url, body, options) - return result - end, + -- Call native implementation + local result = __http_request(method, url, body, options) + return result + end, - -- Simple GET request - get = function(url, options) - return http.client.request("GET", url, nil, options) - end, + -- Simple GET request + get = function(url, options) + return http.client.request("GET", url, nil, options) + end, - -- Simple POST request with automatic content-type - post = function(url, body, options) - options = options or {} - return http.client.request("POST", url, body, options) - end, + -- Simple POST request with automatic content-type + post = function(url, body, options) + options = options or {} + return http.client.request("POST", url, body, options) + end, - -- Simple PUT request with automatic content-type - put = function(url, body, options) - options = options or {} - return http.client.request("PUT", url, body, options) - end, + -- Simple PUT request with automatic content-type + put = function(url, body, options) + options = options or {} + return http.client.request("PUT", url, body, options) + end, - -- Simple DELETE request - delete = function(url, options) - return http.client.request("DELETE", url, nil, options) - end, + -- Simple DELETE request + delete = function(url, options) + return http.client.request("DELETE", url, nil, options) + end, - -- Simple PATCH request - patch = function(url, body, options) - options = options or {} - return http.client.request("PATCH", url, body, options) - end, + -- Simple PATCH request + patch = function(url, body, options) + options = options or {} + return http.client.request("PATCH", url, body, options) + end, - -- Simple HEAD request - head = function(url, options) - options = options or {} - return http.client.request("HEAD", url, nil, options) - end, + -- Simple HEAD request + head = function(url, options) + options = options or {} + return http.client.request("HEAD", url, nil, options) + end, - -- Simple OPTIONS request - options = function(url, options) - return http.client.request("OPTIONS", url, nil, options) - end, + -- Simple OPTIONS request + options = function(url, options) + return http.client.request("OPTIONS", url, nil, options) + end, - -- Shorthand function to directly get JSON - get_json = function(url, options) - options = options or {} - local response = http.client.get(url, options) - if response.ok and response.json then - return response.json - end - return nil, response - end, + -- Shorthand function to directly get JSON + get_json = function(url, options) + options = options or {} + local response = http.client.get(url, options) + if response.ok and response.json then + return response.json + end + return nil, response + end, - -- Utility to build a URL with query parameters - build_url = function(base_url, params) - if not params or type(params) ~= "table" then - return base_url - end + -- Utility to build a URL with query parameters + build_url = function(base_url, params) + if not params or type(params) ~= "table" then + return base_url + end - local query = {} - for k, v in pairs(params) do - if type(v) == "table" then - for _, item in ipairs(v) do - table.insert(query, k .. "=" .. tostring(item)) - end - else - table.insert(query, k .. "=" .. tostring(v)) - end - end + local query = {} + for k, v in pairs(params) do + if type(v) == "table" then + for _, item in ipairs(v) do + table.insert(query, k .. "=" .. tostring(item)) + end + else + table.insert(query, k .. "=" .. tostring(v)) + end + end - if #query > 0 then - if base_url:find("?") then - return base_url .. "&" .. table.concat(query, "&") - else - return base_url .. "?" .. table.concat(query, "&") - end - end + if #query > 0 then + if base_url:find("?") then + return base_url .. "&" .. table.concat(query, "&") + else + return base_url .. "?" .. table.concat(query, "&") + end + end - return base_url - end - } + return base_url + end + } } -- ====================================================================== @@ -235,15 +203,10 @@ local cookie = { error("cookie.set: name must be a string", 2) end - -- Get or create response - local resp = __http_responses[1] or {} + local resp = __ensure_response() resp.cookies = resp.cookies or {} - __http_responses[1] = resp - -- Handle options as table local opts = options or {} - - -- Create cookie table local cookie = { name = name, value = value or "", @@ -251,7 +214,6 @@ local cookie = { domain = opts.domain } - -- Handle expiry if opts.expires then if type(opts.expires) == "number" then if opts.expires > 0 then @@ -266,13 +228,10 @@ local cookie = { end end - -- Security flags cookie.secure = (opts.secure ~= false) cookie.http_only = (opts.http_only ~= false) - -- Store in cookies table - local n = #resp.cookies + 1 - resp.cookies[n] = cookie + table.insert(resp.cookies, cookie) return true end, @@ -283,15 +242,12 @@ local cookie = { error("cookie.get: name must be a string", 2) end - -- Access values directly from current environment local env = getfenv(2) - -- Check if context exists and has cookies if env.ctx and env.ctx.cookies then return env.ctx.cookies[name] end - -- If context has request_cookies map if env.ctx and env.ctx._request_cookies then return env.ctx._request_cookies[name] end @@ -305,185 +261,10 @@ local cookie = { error("cookie.remove: name must be a string", 2) end - -- Create an expired cookie return cookie.set(name, "", {expires = 0, path = path or "/", domain = domain}) 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('', 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 -- ====================================================================== @@ -575,6 +356,4 @@ local util = { -- Install modules in global scope _G.http = http _G.cookie = cookie -_G.session = session -_G.csrf = csrf _G.util = util