202 lines
5.5 KiB
Go
202 lines
5.5 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
|
|
}
|
|
|
|
switch res := result.(type) {
|
|
case string:
|
|
// String result
|
|
w.Header().Set("Content-Type", contentTypePlain)
|
|
w.Write([]byte(res))
|
|
|
|
case map[string]any, []any:
|
|
// Table or array result - convert to JSON
|
|
w.Header().Set("Content-Type", 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)
|
|
|
|
default:
|
|
// Other result types - convert to JSON
|
|
w.Header().Set("Content-Type", contentTypeJSON)
|
|
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)
|
|
}
|
|
}
|