Moonshark/core/http/server.go
2025-03-22 22:27:58 -05:00

214 lines
5.7 KiB
Go

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)
}
}