Moonshark/core/http/Server.go
2025-04-03 12:59:12 -05:00

342 lines
9.2 KiB
Go

package http
import (
"context"
"encoding/json"
"fmt" // Added for fmt.Fprintf
"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.Runner
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.Runner,
loggingEnabled bool, debugMode bool, overrideDir string, config *config.Config) *Server {
server := &Server{
luaRouter: luaRouter,
staticRouter: staticRouter,
luaRunner: runner,
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
logger.ServerCont("Catch the swell at http://localhost%s", addr)
return s.httpServer.ListenAndServe()
}
// Shutdown gracefully shuts down the server
func (s *Server) Shutdown(ctx context.Context) error {
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(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(statusCode, r, duration)
}
}
// handleRequest processes the actual request
func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) {
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()
}
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 {
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()
// Set up context exactly as the original
cookieMap := make(map[string]any)
for _, cookie := range r.Cookies() {
cookieMap[cookie.Name] = cookie.Value
}
ctx.Set("_request_cookies", cookieMap)
ctx.Set("method", r.Method)
ctx.Set("path", r.URL.Path)
ctx.Set("host", r.Host)
// Headers
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)
// Cookies
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)
}
// 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)
}
// Query parameters
queryMap := QueryToLua(r)
if queryMap == nil {
ctx.Set("query", make(map[string]any))
} else {
ctx.Set("query", queryMap)
}
// Form data
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)
// Special handling for CSRF error
if err != nil {
if csrfErr, ok := err.(*runner.CSRFError); ok {
logger.Warning("CSRF error executing Lua route: %v", csrfErr)
HandleCSRFError(w, r, s.errorConfig)
return
}
// Normal error handling
logger.Error("Error executing Lua route: %v", err)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
errorHTML := utils.InternalErrorPage(s.errorConfig, r.URL.Path, err.Error())
w.Write([]byte(errorHTML))
return
}
writeResponse(w, result)
}
// 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) {
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
}
// 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(w, contentTypeJSON)
data, err := json.Marshal(result)
if err != nil {
logger.Error("Failed to marshal response: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Write(data)
return
}
// All other types - convert to plain text
setContentTypeIfMissing(w, contentTypePlain)
switch r := result.(type) {
case string:
w.Write([]byte(r))
case []byte:
w.Write(r)
default:
// Convert any other type to string
fmt.Fprintf(w, "%v", r)
}
}
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, _ *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))
}