diff --git a/http/server.go b/http/server.go index 6d7e14d..8b5cd93 100644 --- a/http/server.go +++ b/http/server.go @@ -2,6 +2,7 @@ package http import ( "context" + "sync" "time" "Moonshark/routers" @@ -26,6 +27,7 @@ type Server struct { config *config.Config sessionManager *sessions.SessionManager errorConfig utils.ErrorPageConfig + ctxPool sync.Pool } // New creates a new HTTP server @@ -45,6 +47,11 @@ func New(luaRouter *routers.LuaRouter, staticRouter *routers.StaticRouter, OverrideDir: overrideDir, DebugMode: debugMode, }, + ctxPool: sync.Pool{ + New: func() any { + return make(map[string]any, 8) + }, + }, } server.fasthttpServer = &fasthttp.Server{ @@ -78,53 +85,49 @@ func (s *Server) Shutdown(ctx context.Context) error { // handleRequest processes the HTTP request func (s *Server) handleRequest(ctx *fasthttp.RequestCtx) { start := time.Now() - method := string(ctx.Method()) - path := string(ctx.Path()) + methodBytes := ctx.Method() + pathBytes := ctx.Path() + + // Only convert to string once + method := string(methodBytes) + path := string(pathBytes) if s.debugMode && path == "/debug/stats" { s.handleDebugStats(ctx) - if s.loggingEnabled { - duration := time.Since(start) - LogRequest(ctx.Response.StatusCode(), method, path, duration) + LogRequest(ctx.Response.StatusCode(), method, path, time.Since(start)) } return } - s.processRequest(ctx) + s.processRequest(ctx, method, path) if s.loggingEnabled { - duration := time.Since(start) - LogRequest(ctx.Response.StatusCode(), method, path, duration) + LogRequest(ctx.Response.StatusCode(), method, path, time.Since(start)) } } // In server.go, modify the processRequest method -func (s *Server) processRequest(ctx *fasthttp.RequestCtx) { - method := string(ctx.Method()) - path := string(ctx.Path()) - +func (s *Server) processRequest(ctx *fasthttp.RequestCtx, method, path string) { logger.Debug("Processing request %s %s", method, path) params := &routers.Params{} bytecode, scriptPath, routeErr, found := s.luaRouter.GetRouteInfo(method, path, params) - // Check if we found a route but it has an error or no valid bytecode if found && (len(bytecode) == 0 || routeErr != nil) { errorMsg := "Route exists but failed to compile. Check server logs for details." - if routeErr != nil { errorMsg = routeErr.Error() } logger.Error("%s %s - %s", method, path, errorMsg) - ctx.SetContentType("text/html; charset=utf-8") ctx.SetStatusCode(fasthttp.StatusInternalServerError) - errorHTML := utils.InternalErrorPage(s.errorConfig, path, errorMsg) - ctx.SetBody([]byte(errorHTML)) + ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, path, errorMsg))) return - } else if found { + } + + if found { logger.Debug("Found Lua route match for %s %s with %d params", method, path, params.Count) s.handleLuaRoute(ctx, bytecode, scriptPath, params, method, path) return @@ -143,58 +146,62 @@ func (s *Server) processRequest(ctx *fasthttp.RequestCtx) { } // handleLuaRoute executes a Lua route -func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scriptPath string, params *routers.Params, method string, path string) { - // Create context for Lua execution +func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scriptPath string, params *routers.Params, method, path string) { luaCtx := runner.NewHTTPContext(ctx) defer luaCtx.Release() - host := string(ctx.Host()) + // Get pooled map for session data + sessionMap := s.ctxPool.Get().(map[string]any) + defer func() { + // Clear and return to pool + for k := range sessionMap { + delete(sessionMap, k) + } + s.ctxPool.Put(sessionMap) + }() + + session := s.sessionManager.GetSessionFromRequest(ctx) + sessionMap["id"] = session.ID + sessionMap["data"] = session.Data luaCtx.Set("method", method) luaCtx.Set("path", path) - luaCtx.Set("host", host) - - session := s.sessionManager.GetSessionFromRequest(ctx) - luaCtx.Set("session", map[string]any{ - "id": session.ID, - "data": session.Data, - }) + luaCtx.Set("host", string(ctx.Host())) // Only convert when needed + luaCtx.Set("session", sessionMap) + // Optimize params handling if params.Count > 0 { - paramMap := make(map[string]any, params.Count) + paramMap := make(map[string]any, params.Count) // Pre-size for i, key := range params.Keys { paramMap[key] = params.Values[i] } luaCtx.Set("params", paramMap) } else { - luaCtx.Set("params", make(map[string]any)) + luaCtx.Set("params", emptyMap) // Reuse empty map } + // Optimize form handling for POST methods if method == "POST" || method == "PUT" || method == "PATCH" { - formData, err := ParseForm(ctx) - if err == nil && len(formData) > 0 { + if formData, err := ParseForm(ctx); err == nil { luaCtx.Set("form", formData) - } else if err != nil { - logger.Warning("Error parsing form: %v", err) - luaCtx.Set("form", make(map[string]any)) } else { - luaCtx.Set("form", make(map[string]any)) + logger.Warning("Error parsing form: %v", err) + luaCtx.Set("form", emptyMap) } } else { - luaCtx.Set("form", make(map[string]any)) + luaCtx.Set("form", emptyMap) } response, err := s.luaRunner.Run(bytecode, luaCtx, scriptPath) if err != nil { logger.Error("Error executing Lua route: %v", err) - ctx.SetContentType("text/html; charset=utf-8") ctx.SetStatusCode(fasthttp.StatusInternalServerError) - errorHTML := utils.InternalErrorPage(s.errorConfig, path, err.Error()) - ctx.SetBody([]byte(errorHTML)) + ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, path, err.Error()))) return } + // Session handling optimization if _, clearAll := response.SessionData["__clear_all"]; clearAll { session.Clear() delete(response.SessionData, "__clear_all") @@ -207,8 +214,8 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip session.Set(k, v) } } - s.sessionManager.ApplySessionCookie(ctx, session) + s.sessionManager.ApplySessionCookie(ctx, session) runner.ApplyResponse(response, ctx) runner.ReleaseResponse(response) } diff --git a/http/utils.go b/http/utils.go index 2618dad..5a734be 100644 --- a/http/utils.go +++ b/http/utils.go @@ -6,6 +6,7 @@ import ( "fmt" "mime/multipart" "strings" + "sync" "time" "Moonshark/utils/logger" @@ -13,63 +14,91 @@ import ( "github.com/valyala/fasthttp" ) +var emptyMap = make(map[string]any) + +var ( + stringPool = sync.Pool{ + New: func() any { + return make([]string, 0, 4) + }, + } + formDataPool = sync.Pool{ + New: func() any { + return make(map[string]any, 16) + }, + } +) + // LogRequest logs an HTTP request with its status code and duration func LogRequest(statusCode int, method, path string, duration time.Duration) { - var statusColor, resetColor, methodColor string + var statusColor, methodColor string - if statusCode >= 200 && statusCode < 300 { + // Simplified color assignment + switch { + case statusCode < 300: statusColor = "\u001b[32m" // Green for 2xx - } else if statusCode >= 300 && statusCode < 400 { + case statusCode < 400: statusColor = "\u001b[36m" // Cyan for 3xx - } else if statusCode >= 400 && statusCode < 500 { + case statusCode < 500: statusColor = "\u001b[33m" // Yellow for 4xx - } else { - statusColor = "\u001b[31m" // Red for 5xx and others + default: + statusColor = "\u001b[31m" // Red for 5xx+ } switch method { case "GET": - methodColor = "\u001b[32m" // Green + methodColor = "\u001b[32m" case "POST": - methodColor = "\u001b[34m" // Blue + methodColor = "\u001b[34m" case "PUT": - methodColor = "\u001b[33m" // Yellow + methodColor = "\u001b[33m" case "DELETE": - methodColor = "\u001b[31m" // Red + methodColor = "\u001b[31m" default: - methodColor = "\u001b[35m" // Magenta for others + methodColor = "\u001b[35m" } - resetColor = "\u001b[0m" - + // Optimized duration formatting var durationStr string - if duration.Milliseconds() < 1 { - durationStr = fmt.Sprintf("%.2fµs", float64(duration.Microseconds())) - } else if duration.Milliseconds() < 1000 { - durationStr = fmt.Sprintf("%.2fms", float64(duration.Microseconds())/1000) + micros := duration.Microseconds() + if micros < 1000 { + durationStr = fmt.Sprintf("%.0fµs", float64(micros)) + } else if micros < 1000000 { + durationStr = fmt.Sprintf("%.1fms", float64(micros)/1000) } else { durationStr = fmt.Sprintf("%.2fs", duration.Seconds()) } - logger.Server("%s%d%s %s%s%s %s %s", - statusColor, statusCode, resetColor, - methodColor, method, resetColor, + logger.Server("%s%d\u001b[0m %s%s\u001b[0m %s %s", + statusColor, statusCode, + methodColor, method, path, durationStr) } // QueryToLua converts HTTP query args to a Lua-friendly map func QueryToLua(ctx *fasthttp.RequestCtx) map[string]any { - queryMap := make(map[string]any) + args := ctx.QueryArgs() + if args.Len() == 0 { + return emptyMap + } - ctx.QueryArgs().VisitAll(func(key, value []byte) { + queryMap := make(map[string]any, args.Len()) // Pre-size + + args.VisitAll(func(key, value []byte) { k := string(key) v := string(value) - if existing, ok := queryMap[k]; ok { - if arr, ok := existing.([]string); ok { - queryMap[k] = append(arr, v) - } else if str, ok := existing.(string); ok { - queryMap[k] = []string{str, v} + if existing, exists := queryMap[k]; exists { + // Handle multiple values more efficiently + switch typed := existing.(type) { + case []string: + queryMap[k] = append(typed, v) + case string: + // Get slice from pool + slice := stringPool.Get().([]string) + slice = slice[:0] // Reset length + slice = append(slice, typed, v) + queryMap[k] = slice } } else { queryMap[k] = v @@ -81,21 +110,36 @@ func QueryToLua(ctx *fasthttp.RequestCtx) map[string]any { // ParseForm extracts form data from a request func ParseForm(ctx *fasthttp.RequestCtx) (map[string]any, error) { - formData := make(map[string]any) + contentType := string(ctx.Request.Header.ContentType()) - if strings.Contains(string(ctx.Request.Header.ContentType()), "multipart/form-data") { + if strings.Contains(contentType, "multipart/form-data") { return parseMultipartForm(ctx) } - ctx.PostArgs().VisitAll(func(key, value []byte) { + args := ctx.PostArgs() + if args.Len() == 0 { + return emptyMap, nil + } + + formData := formDataPool.Get().(map[string]any) + // Clear the map (should already be clean from pool) + for k := range formData { + delete(formData, k) + } + + args.VisitAll(func(key, value []byte) { k := string(key) v := string(value) - if existing, ok := formData[k]; ok { - if arr, ok := existing.([]string); ok { - formData[k] = append(arr, v) - } else if str, ok := existing.(string); ok { - formData[k] = []string{str, v} + if existing, exists := formData[k]; exists { + switch typed := existing.(type) { + case []string: + formData[k] = append(typed, v) + case string: + slice := stringPool.Get().([]string) + slice = slice[:0] + slice = append(slice, typed, v) + formData[k] = slice } } else { formData[k] = v @@ -107,36 +151,43 @@ func ParseForm(ctx *fasthttp.RequestCtx) (map[string]any, error) { // parseMultipartForm handles multipart/form-data requests func parseMultipartForm(ctx *fasthttp.RequestCtx) (map[string]any, error) { - formData := make(map[string]any) - form, err := ctx.MultipartForm() if err != nil { return nil, err } + formData := formDataPool.Get().(map[string]any) + for k := range formData { + delete(formData, k) + } + + // Process form values for key, values := range form.Value { - if len(values) == 1 { + switch len(values) { + case 0: + // Skip empty + case 1: formData[key] = values[0] - } else if len(values) > 1 { + default: formData[key] = values } } + // Process files if present if len(form.File) > 0 { - files := make(map[string]any) - + files := make(map[string]any, len(form.File)) for fieldName, fileHeaders := range form.File { - if len(fileHeaders) == 1 { + switch len(fileHeaders) { + case 1: files[fieldName] = fileInfoToMap(fileHeaders[0]) - } else if len(fileHeaders) > 1 { - fileInfos := make([]map[string]any, 0, len(fileHeaders)) - for _, fh := range fileHeaders { - fileInfos = append(fileInfos, fileInfoToMap(fh)) + default: + fileInfos := make([]map[string]any, len(fileHeaders)) + for i, fh := range fileHeaders { + fileInfos[i] = fileInfoToMap(fh) } files[fieldName] = fileInfos } } - formData["_files"] = files } diff --git a/runner/lua/sandbox.lua b/runner/lua/sandbox.lua index 97d67c7..fd00ce8 100644 --- a/runner/lua/sandbox.lua +++ b/runner/lua/sandbox.lua @@ -438,17 +438,14 @@ _G.render = function(template_str, env) local pos, chunks = 1, {} while pos <= #template_str do - local escaped_start = template_str:find("<%?", pos, true) - local unescaped_start = template_str:find("" or tag_type == "-" and "!>" or "%>" + local close_tag = tag_type == "-" and "}}}" or "}}" local close_start, close_stop = template_str:find(close_tag, pos, true) if not close_start then error("Failed to find closing tag at position " .. pos) end - local trim_newline = false - if template_str:sub(close_start-1, close_start-1) == "-" then - close_start = close_start - 1 - trim_newline = true - end - - local code = template_str:sub(pos, close_start-1) - table.insert(chunks, {tag_type, code, pos}) - + local code = template_str:sub(pos, close_start-1):match("^%s*(.-)%s*$") + + -- Check if it's a simple variable name for escaped output + local is_simple_var = tag_type == "=" and code:match("^[%w_]+$") + + table.insert(chunks, {tag_type, code, pos, is_simple_var}) pos = close_stop + 1 - if trim_newline and template_str:sub(pos, pos) == "\n" then - pos = pos + 1 - end end local buffer = {"local _tostring, _escape, _b, _b_i = ...\n"} @@ -488,16 +479,16 @@ _G.render = function(template_str, env) table.insert(buffer, "_b[_b_i] = " .. string.format("%q", chunk) .. "\n") else t = chunk[1] - if t == "code" then - table.insert(buffer, "--[[" .. chunk[3] .. "]] " .. chunk[2] .. "\n") - elseif t == "=" or t == "-" then - table.insert(buffer, "_b_i = _b_i + 1\n") - table.insert(buffer, "--[[" .. chunk[3] .. "]] _b[_b_i] = ") - if t == "=" then - table.insert(buffer, "_escape(_tostring(" .. chunk[2] .. "))\n") + if t == "=" then + if chunk[4] then -- is_simple_var + table.insert(buffer, "_b_i = _b_i + 1\n") + table.insert(buffer, "--[[" .. chunk[3] .. "]] _b[_b_i] = _escape(_tostring(" .. chunk[2] .. "))\n") else - table.insert(buffer, "_tostring(" .. chunk[2] .. ")\n") + table.insert(buffer, "--[[" .. chunk[3] .. "]] " .. chunk[2] .. "\n") end + elseif t == "-" then + table.insert(buffer, "_b_i = _b_i + 1\n") + table.insert(buffer, "--[[" .. chunk[3] .. "]] _b[_b_i] = _tostring(" .. chunk[2] .. ")\n") end end end @@ -521,14 +512,14 @@ _G.parse = function(template_str, env) env = env or {} while pos <= #template_str do - local escaped_start, escaped_end, escaped_name = template_str:find("<%?%s*([%w_]+)%s*%?>", pos) - local unescaped_start, unescaped_end, unescaped_name = template_str:find("", pos) + local unescaped_start, unescaped_end, unescaped_name = template_str:find("{{{%s*([%w_]+)%s*}}}", pos) + local escaped_start, escaped_end, escaped_name = template_str:find("{{%s*([%w_]+)%s*}}", pos) local next_pos, placeholder_end, name, escaped - if escaped_start and (not unescaped_start or escaped_start < unescaped_start) then - next_pos, placeholder_end, name, escaped = escaped_start, escaped_end, escaped_name, true - elseif unescaped_start then + if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then next_pos, placeholder_end, name, escaped = unescaped_start, unescaped_end, unescaped_name, false + elseif escaped_start then + next_pos, placeholder_end, name, escaped = escaped_start, escaped_end, escaped_name, true else local text = template_str:sub(pos) if text and #text > 0 then @@ -561,14 +552,14 @@ _G.iparse = function(template_str, values) values = values or {} while pos <= #template_str do - local escaped_start, escaped_end = template_str:find("<%?>", pos, true) - local unescaped_start, unescaped_end = template_str:find("", pos, true) + local unescaped_start, unescaped_end = template_str:find("{{{}}}", pos, true) + local escaped_start, escaped_end = template_str:find("{{}}", pos, true) local next_pos, placeholder_end, escaped - if escaped_start and (not unescaped_start or escaped_start < unescaped_start) then - next_pos, placeholder_end, escaped = escaped_start, escaped_end, true - elseif unescaped_start then + if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then next_pos, placeholder_end, escaped = unescaped_start, unescaped_end, false + elseif escaped_start then + next_pos, placeholder_end, escaped = escaped_start, escaped_end, true else local text = template_str:sub(pos) if text and #text > 0 then