Moonshark/core/http/server.go
2025-03-07 07:43:04 -06:00

185 lines
4.9 KiB
Go

package http
import (
"context"
"encoding/json"
"net/http"
"time"
"git.sharkk.net/Sky/Moonshark/core/logger"
"git.sharkk.net/Sky/Moonshark/core/routers"
"git.sharkk.net/Sky/Moonshark/core/workers"
)
// Server handles HTTP requests using Lua and static file routers
type Server struct {
luaRouter *routers.LuaRouter
staticRouter *routers.StaticRouter
workerPool *workers.Pool
logger *logger.Logger
httpServer *http.Server
}
// New creates a new HTTP server
func New(luaRouter *routers.LuaRouter, staticRouter *routers.StaticRouter, pool *workers.Pool, log *logger.Logger) *Server {
server := &Server{
luaRouter: luaRouter,
staticRouter: staticRouter,
workerPool: pool,
logger: log,
httpServer: &http.Server{},
}
server.httpServer.Handler = server
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
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, 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, 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, params *routers.Params) {
ctx := workers.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)
ctx.Set("headers", makeHeaderMap(r.Header))
// 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)
}
// Add query parameters
if queryParams := QueryToLua(r); queryParams != nil {
ctx.Set("query", queryParams)
}
// Add 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.workerPool.Submit(bytecode, ctx)
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)
}
// makeHeaderMap converts HTTP headers to a map
func makeHeaderMap(header http.Header) map[string]any {
result := make(map[string]any, len(header))
for name, values := range header {
if len(values) == 1 {
result[name] = values[0]
} else {
result[name] = values
}
}
return result
}
// 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
}
switch res := result.(type) {
case string:
// String result
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(res))
case map[string]any:
// Table result - convert to JSON
w.Header().Set("Content-Type", "application/json")
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)
default:
// Other result types - convert to JSON
w.Header().Set("Content-Type", "application/json")
data, err := json.Marshal(result)
if err != nil {
log.Error("Failed to marshal response: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Write(data)
}
}