package http import ( "context" "fmt" "time" "Moonshark/core/metadata" "Moonshark/core/routers" "Moonshark/core/runner" "Moonshark/core/runner/sandbox" "Moonshark/core/utils" "Moonshark/core/utils/config" "Moonshark/core/utils/logger" "github.com/goccy/go-json" "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 errorConfig utils.ErrorPageConfig } // New creates a new HTTP server with optimized connection settings 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, errorConfig: utils.ErrorPageConfig{ OverrideDir: overrideDir, DebugMode: debugMode, }, } // Configure fasthttp server server.fasthttpServer = &fasthttp.Server{ Handler: server.handleRequest, Name: "Moonshark/" + metadata.Version, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, MaxRequestBodySize: 16 << 20, // 16MB - consistent with Forms.go DisableKeepalive: false, TCPKeepalive: true, TCPKeepalivePeriod: 60 * time.Second, ReduceMemoryUsage: true, GetOnly: false, DisablePreParseMultipartForm: true, // We'll handle parsing manually } return server } // ListenAndServe starts the server on the given address func (s *Server) ListenAndServe(addr string) error { logger.ServerCont("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() method := string(ctx.Method()) path := string(ctx.Path()) // Special case for debug stats when debug mode is enabled if s.debugMode && path == "/debug/stats" { s.handleDebugStats(ctx) // Log request if s.loggingEnabled { duration := time.Since(start) LogRequest(ctx.Response.StatusCode(), method, path, duration) } return } // Process the request s.processRequest(ctx) // Log the request with our custom format if s.loggingEnabled { duration := time.Since(start) LogRequest(ctx.Response.StatusCode(), method, path, duration) } } // processRequest processes the actual request func (s *Server) processRequest(ctx *fasthttp.RequestCtx) { method := string(ctx.Method()) path := string(ctx.Path()) logger.Debug("Processing request %s %s", method, path) // Try Lua routes first params := &routers.Params{} bytecode, scriptPath, found := s.luaRouter.GetBytecode(method, path, params) // Check if we found a route but it has no valid bytecode (compile error) if found && len(bytecode) == 0 { // Get the actual error from the router errorMsg := "Route exists but failed to compile. Check server logs for details." // Get the actual node to access its error if node, _ := s.luaRouter.GetNodeWithError(method, path, params); node != nil && node.Error != nil { errorMsg = node.Error.Error() } logger.Error("%s %s - %s", method, path, errorMsg) // Show error page with the actual error message ctx.SetContentType("text/html; charset=utf-8") ctx.SetStatusCode(fasthttp.StatusInternalServerError) errorHTML := utils.InternalErrorPage(s.errorConfig, path, errorMsg) ctx.SetBody([]byte(errorHTML)) return } else if found { logger.Debug("Found Lua route match for %s %s with %d params", method, path, params.Count) s.handleLuaRoute(ctx, bytecode, scriptPath, params) return } // Then try static files if _, found := s.staticRouter.Match(path); found { s.staticRouter.ServeHTTP(ctx) return } // No route found - 404 Not Found ctx.SetContentType("text/html; charset=utf-8") ctx.SetStatusCode(fasthttp.StatusNotFound) ctx.SetBody([]byte(utils.NotFoundPage(s.errorConfig, path))) } // HandleMethodNotAllowed responds with a 405 Method Not Allowed error func HandleMethodNotAllowed(ctx *fasthttp.RequestCtx, errorConfig utils.ErrorPageConfig) { path := string(ctx.Path()) ctx.SetContentType("text/html; charset=utf-8") ctx.SetStatusCode(fasthttp.StatusMethodNotAllowed) ctx.SetBody([]byte(utils.MethodNotAllowedPage(errorConfig, path))) } // handleLuaRoute executes a Lua route func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scriptPath string, params *routers.Params) { luaCtx := runner.NewContext() defer luaCtx.Release() method := string(ctx.Method()) path := string(ctx.Path()) host := string(ctx.Host()) // Set up context luaCtx.Set("method", method) luaCtx.Set("path", path) luaCtx.Set("host", host) // Headers headerMap := make(map[string]any) ctx.Request.Header.VisitAll(func(key, value []byte) { headerMap[string(key)] = string(value) }) luaCtx.Set("headers", headerMap) // Cookies cookieMap := make(map[string]any) ctx.Request.Header.VisitAllCookie(func(key, value []byte) { cookieMap[string(key)] = string(value) }) if len(cookieMap) > 0 { luaCtx.Set("cookies", cookieMap) luaCtx.Set("_request_cookies", cookieMap) // For backward compatibility } else { luaCtx.Set("cookies", make(map[string]any)) luaCtx.Set("_request_cookies", make(map[string]any)) } // URL parameters if params.Count > 0 { paramMap := make(map[string]any, params.Count) for i, key := range params.Keys { paramMap[key] = params.Values[i] } luaCtx.Set("params", paramMap) } else { luaCtx.Set("params", make(map[string]any)) } // Query parameters queryMap := QueryToLua(ctx) luaCtx.Set("query", queryMap) // Form data if method == "POST" || method == "PUT" || method == "PATCH" { formData, err := ParseForm(ctx) if err == nil && len(formData) > 0 { 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)) } } else { luaCtx.Set("form", make(map[string]any)) } // Execute Lua script result, err := s.luaRunner.Run(bytecode, luaCtx, scriptPath) // Special handling for CSRF error if err != nil { if csrfErr, ok := err.(*CSRFError); ok { logger.Warning("CSRF error executing Lua route: %v", csrfErr) HandleCSRFError(ctx, s.errorConfig) return } // Normal error handling 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)) return } writeResponse(ctx, result) } // Content types for responses const ( contentTypeJSON = "application/json" contentTypePlain = "text/plain" ) // writeResponse writes the Lua result to the HTTP response func writeResponse(ctx *fasthttp.RequestCtx, result any) { if result == nil { ctx.SetStatusCode(fasthttp.StatusNoContent) return } // Check for HTTPResponse type if httpResp, ok := result.(*sandbox.HTTPResponse); ok { defer sandbox.ReleaseResponse(httpResp) // Set response headers for name, value := range httpResp.Headers { ctx.Response.Header.Set(name, value) } // Set cookies for _, cookie := range httpResp.Cookies { ctx.Response.Header.SetCookie(cookie) } // Set status code ctx.SetStatusCode(httpResp.Status) // Process the body based on its type if httpResp.Body == nil { return } result = httpResp.Body // Set result to body for processing below } // Check if it's a map (table) or array - return as JSON isJSON := false switch result.(type) { case map[string]any, []any, []float64, []string, []int: isJSON = true } if isJSON { setContentTypeIfMissing(ctx, contentTypeJSON) data, err := json.Marshal(result) if err != nil { logger.Error("Failed to marshal response: %v", err) ctx.Error("Internal Server Error", fasthttp.StatusInternalServerError) return } ctx.SetBody(data) return } // All other types - convert to plain text setContentTypeIfMissing(ctx, contentTypePlain) switch r := result.(type) { case string: ctx.SetBodyString(r) case []byte: ctx.SetBody(r) default: // Convert any other type to string ctx.SetBodyString(fmt.Sprintf("%v", r)) } } func setContentTypeIfMissing(ctx *fasthttp.RequestCtx, contentType string) { if len(ctx.Response.Header.ContentType()) == 0 { ctx.SetContentType(contentType) } } // handleDebugStats displays debug statistics func (s *Server) handleDebugStats(ctx *fasthttp.RequestCtx) { // Collect system stats stats := utils.CollectSystemStats(s.config) // Add component stats routeCount, bytecodeBytes := s.luaRouter.GetRouteStats() moduleCount := s.luaRunner.GetModuleCount() stats.Components = utils.ComponentStats{ RouteCount: routeCount, BytecodeBytes: bytecodeBytes, ModuleCount: moduleCount, } // Generate HTML page html := utils.DebugStatsPage(stats) // Send the response ctx.SetContentType("text/html; charset=utf-8") ctx.SetStatusCode(fasthttp.StatusOK) ctx.SetBody([]byte(html)) }