package http import ( "context" "encoding/json" "net" "net/http" "time" "git.sharkk.net/Sky/Moonshark/core/logger" "git.sharkk.net/Sky/Moonshark/core/routers" "git.sharkk.net/Sky/Moonshark/core/runner" ) // Server handles HTTP requests using Lua and static file routers type Server struct { luaRouter *routers.LuaRouter staticRouter *routers.StaticRouter luaRunner *runner.LuaRunner logger *logger.Logger httpServer *http.Server loggingEnabled bool } // New creates a new HTTP server with optimized connection settings func New(luaRouter *routers.LuaRouter, staticRouter *routers.StaticRouter, runner *runner.LuaRunner, log *logger.Logger, loggingEnabled bool) *Server { server := &Server{ luaRouter: luaRouter, staticRouter: staticRouter, luaRunner: runner, logger: log, httpServer: &http.Server{}, loggingEnabled: loggingEnabled, } server.httpServer.Handler = server // Set TCP keep-alive for connections server.httpServer.ConnState = func(conn net.Conn, state http.ConnState) { if state == http.StateNew { if tcpConn, ok := conn.(*net.TCPConn); ok { tcpConn.SetKeepAlive(true) } } } return server } // ListenAndServe starts the server on the given address func (s *Server) ListenAndServe(addr string) error { s.httpServer.Addr = addr s.logger.Info("Server listening at http://localhost%s", addr) return s.httpServer.ListenAndServe() } // Shutdown gracefully shuts down the server func (s *Server) Shutdown(ctx context.Context) error { s.logger.Info("Server shutting down...") return s.httpServer.Shutdown(ctx) } // ServeHTTP handles HTTP requests func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { start := time.Now() // Wrap the ResponseWriter to capture status code wrappedWriter := newStatusCaptureWriter(w) // Process the request s.handleRequest(wrappedWriter, r) // Calculate request duration duration := time.Since(start) // Get the status code statusCode := wrappedWriter.StatusCode() // Log the request with our custom format if s.loggingEnabled { LogRequest(s.logger, statusCode, r, duration) } } // handleRequest processes the actual request func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) { s.logger.Debug("Processing request %s %s", r.Method, r.URL.Path) // Try Lua routes first params := &routers.Params{} if bytecode, scriptPath, found := s.luaRouter.GetBytecode(r.Method, r.URL.Path, params); found { s.logger.Debug("Found Lua route match for %s %s with %d params", r.Method, r.URL.Path, params.Count) s.handleLuaRoute(w, r, bytecode, scriptPath, params) return } // Then try static files if filePath, found := s.staticRouter.Match(r.URL.Path); found { http.ServeFile(w, r, filePath) return } // No route found http.NotFound(w, r) } // handleLuaRoute executes a Lua route func (s *Server) handleLuaRoute(w http.ResponseWriter, r *http.Request, bytecode []byte, scriptPath string, params *routers.Params) { ctx := runner.NewContext() // Log bytecode size s.logger.Debug("Executing Lua route with %d bytes of bytecode", len(bytecode)) // Add request info directly to context ctx.Set("method", r.Method) ctx.Set("path", r.URL.Path) ctx.Set("host", r.Host) // Inline the header conversion (previously makeHeaderMap) headerMap := make(map[string]any, len(r.Header)) for name, values := range r.Header { if len(values) == 1 { headerMap[name] = values[0] } else { headerMap[name] = values } } ctx.Set("headers", headerMap) // Add URL parameters if params.Count > 0 { paramMap := make(map[string]any, params.Count) for i := 0; i < params.Count; i++ { paramMap[params.Keys[i]] = params.Values[i] } ctx.Set("params", paramMap) } // Query parameters will be parsed lazily via metatable in Lua // Instead of parsing for every request, we'll pass the raw URL ctx.Set("rawQuery", r.URL.RawQuery) // Add form data for POST/PUT/PATCH only when needed if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodPatch { if formData, err := ParseForm(r); err == nil && len(formData) > 0 { ctx.Set("form", formData) } } // Execute Lua script result, err := s.luaRunner.Run(bytecode, ctx, scriptPath) if err != nil { s.logger.Error("Error executing Lua route: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } writeResponse(w, result, s.logger) } // Content types for responses const ( contentTypeJSON = "application/json" contentTypePlain = "text/plain" ) // writeResponse writes the Lua result to the HTTP response func writeResponse(w http.ResponseWriter, result any, log *logger.Logger) { if result == nil { w.WriteHeader(http.StatusNoContent) return } // Check for HTTPResponse type if httpResp, ok := result.(*runner.HTTPResponse); ok { // Set response headers for name, value := range httpResp.Headers { w.Header().Set(name, value) } // Set status code w.WriteHeader(httpResp.Status) // Process the body based on its type if httpResp.Body == nil { return } result = httpResp.Body // Set result to body for processing below } switch res := result.(type) { case string: // String result - plain text setContentTypeIfMissing(w, contentTypePlain) w.Write([]byte(res)) default: // All other types - convert to JSON setContentTypeIfMissing(w, contentTypeJSON) data, err := json.Marshal(res) if err != nil { log.Error("Failed to marshal response: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } w.Write(data) } } func setContentTypeIfMissing(w http.ResponseWriter, contentType string) { if w.Header().Get("Content-Type") == "" { w.Header().Set("Content-Type", contentType) } }