Moonshark/core/http/server.go

323 lines
9.0 KiB
Go

package http
import (
"context"
"encoding/json"
"net"
"net/http"
"time"
"git.sharkk.net/Sky/Moonshark/core/config"
"git.sharkk.net/Sky/Moonshark/core/logger"
"git.sharkk.net/Sky/Moonshark/core/routers"
"git.sharkk.net/Sky/Moonshark/core/runner"
"git.sharkk.net/Sky/Moonshark/core/utils"
)
// 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
debugMode bool // Controls whether to show error details
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.LuaRunner, log *logger.Logger,
loggingEnabled bool, debugMode bool, overrideDir string, config *config.Config) *Server {
server := &Server{
luaRouter: luaRouter,
staticRouter: staticRouter,
luaRunner: runner,
logger: log,
httpServer: &http.Server{},
loggingEnabled: loggingEnabled,
debugMode: debugMode,
config: config,
errorConfig: utils.ErrorPageConfig{
OverrideDir: overrideDir,
DebugMode: debugMode,
},
}
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()
// Special case for debug stats when debug mode is enabled
if s.debugMode && r.URL.Path == "/debug/stats" {
s.handleDebugStats(w, r)
// Calculate and log request duration
duration := time.Since(start)
if s.loggingEnabled {
LogRequest(s.logger, http.StatusOK, r, duration)
}
return
}
// 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{}
bytecode, scriptPath, found := s.luaRouter.GetBytecode(r.Method, r.URL.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 - this requires exposing the actual error
// from the node in the GetBytecode method
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(r.Method, r.URL.Path, params); node != nil && node.Error != nil {
errorMsg = node.Error.Error()
}
s.logger.Error("%s %s - %s", r.Method, r.URL.Path, errorMsg)
// Show error page with the actual error message
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
errorHTML := utils.InternalErrorPage(s.errorConfig, r.URL.Path, errorMsg)
w.Write([]byte(errorHTML))
return
} else if 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 - 404 Not Found
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(utils.NotFoundPage(s.errorConfig, r.URL.Path)))
}
// HandleMethodNotAllowed responds with a 405 Method Not Allowed error
func (s *Server) HandleMethodNotAllowed(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte(utils.MethodNotAllowedPage(s.errorConfig, r.URL.Path)))
}
// 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()
defer ctx.Release()
// 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)
// Add headers to context
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 cookies to context
if cookies := r.Cookies(); len(cookies) > 0 {
cookieMap := make(map[string]any, len(cookies))
for _, cookie := range cookies {
cookieMap[cookie.Name] = cookie.Value
}
ctx.Set("cookies", cookieMap)
}
// Add URL parameters
if params.Count > 0 {
paramMap := make(map[string]any, params.Count)
for i, key := range params.Keys {
paramMap[key] = params.Values[i]
}
ctx.Set("params", paramMap)
}
// Parse query parameters only if present
queryMap := QueryToLua(r)
if queryMap == nil {
ctx.Set("query", make(map[string]any))
} else {
ctx.Set("query", queryMap)
}
// 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)
// Set content type to HTML
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
// Generate error page with error message
errorMsg := err.Error()
errorHTML := utils.InternalErrorPage(s.errorConfig, r.URL.Path, errorMsg)
w.Write([]byte(errorHTML))
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 {
defer runner.ReleaseResponse(httpResp)
// Set response headers
for name, value := range httpResp.Headers {
w.Header().Set(name, value)
}
// Set cookies
for _, cookie := range httpResp.Cookies {
http.SetCookie(w, cookie)
}
// 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 - default to 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)
}
}
// handleDebugStats displays debug statistics
func (s *Server) handleDebugStats(w http.ResponseWriter, r *http.Request) {
// 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
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte(html))
}