package http import ( "context" "sync" "time" "Moonshark/routers" "Moonshark/runner" "Moonshark/sessions" "Moonshark/utils" "Moonshark/utils/config" "Moonshark/utils/logger" "Moonshark/utils/metadata" "github.com/valyala/fasthttp" ) // Server handles HTTP requests using Lua and static file routers type Server struct { luaRouter *routers.LuaRouter staticRouter *routers.StaticRouter luaRunner *runner.Runner fasthttpServer *fasthttp.Server loggingEnabled bool debugMode bool config *config.Config sessionManager *sessions.SessionManager errorConfig utils.ErrorPageConfig ctxPool sync.Pool } // New creates a new HTTP server func New(luaRouter *routers.LuaRouter, staticRouter *routers.StaticRouter, runner *runner.Runner, loggingEnabled bool, debugMode bool, overrideDir string, config *config.Config) *Server { server := &Server{ luaRouter: luaRouter, staticRouter: staticRouter, luaRunner: runner, loggingEnabled: loggingEnabled, debugMode: debugMode, config: config, sessionManager: sessions.GlobalSessionManager, errorConfig: utils.ErrorPageConfig{ OverrideDir: overrideDir, DebugMode: debugMode, }, ctxPool: sync.Pool{ New: func() any { return make(map[string]any, 8) }, }, } server.fasthttpServer = &fasthttp.Server{ Handler: server.handleRequest, Name: "Moonshark/" + metadata.Version, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, MaxRequestBodySize: 16 << 20, // 16MB DisableKeepalive: false, TCPKeepalive: true, TCPKeepalivePeriod: 60 * time.Second, ReduceMemoryUsage: true, GetOnly: false, DisablePreParseMultipartForm: true, } return server } // ListenAndServe starts the server on the given address func (s *Server) ListenAndServe(addr string) error { logger.Server("Catch the swell at http://localhost%s", addr) return s.fasthttpServer.ListenAndServe(addr) } // Shutdown gracefully shuts down the server func (s *Server) Shutdown(ctx context.Context) error { return s.fasthttpServer.ShutdownWithContext(ctx) } // handleRequest processes the HTTP request func (s *Server) handleRequest(ctx *fasthttp.RequestCtx) { start := time.Now() 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 { LogRequest(ctx.Response.StatusCode(), method, path, time.Since(start)) } return } s.processRequest(ctx, method, path) if s.loggingEnabled { LogRequest(ctx.Response.StatusCode(), method, path, time.Since(start)) } } // In server.go, modify the processRequest method 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) 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) ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, path, errorMsg))) return } 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 } if s.staticRouter != nil { if _, found := s.staticRouter.Match(path); found { s.staticRouter.ServeHTTP(ctx) return } } ctx.SetContentType("text/html; charset=utf-8") ctx.SetStatusCode(fasthttp.StatusNotFound) ctx.SetBody([]byte(utils.NotFoundPage(s.errorConfig, path))) } // handleLuaRoute executes a Lua route func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scriptPath string, params *routers.Params, method, path string) { luaCtx := runner.NewHTTPContext(ctx) defer luaCtx.Release() // 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", 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) // Pre-size for i, key := range params.Keys { paramMap[key] = params.Values[i] } luaCtx.Set("params", paramMap) } else { luaCtx.Set("params", emptyMap) // Reuse empty map } // Optimize form handling for POST methods if method == "POST" || method == "PUT" || method == "PATCH" { if formData, err := ParseForm(ctx); err == nil { luaCtx.Set("form", formData) } else { logger.Warning("Error parsing form: %v", err) luaCtx.Set("form", emptyMap) } } else { 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) 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") } for k, v := range response.SessionData { if v == "__SESSION_DELETE_MARKER__" { session.Delete(k) } else { session.Set(k, v) } } s.sessionManager.ApplySessionCookie(ctx, session) runner.ApplyResponse(response, ctx) runner.ReleaseResponse(response) } // handleDebugStats displays debug statistics func (s *Server) handleDebugStats(ctx *fasthttp.RequestCtx) { stats := utils.CollectSystemStats(s.config) routeCount, bytecodeBytes := s.luaRouter.GetRouteStats() stats.Components = utils.ComponentStats{ RouteCount: routeCount, BytecodeBytes: bytecodeBytes, SessionStats: sessions.GlobalSessionManager.GetCacheStats(), } ctx.SetContentType("text/html; charset=utf-8") ctx.SetStatusCode(fasthttp.StatusOK) ctx.SetBody([]byte(utils.DebugStatsPage(stats))) }