267 lines
7.6 KiB
Go
267 lines
7.6 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"
|
|
"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
|
|
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) *Server {
|
|
|
|
server := &Server{
|
|
luaRouter: luaRouter,
|
|
staticRouter: staticRouter,
|
|
luaRunner: runner,
|
|
logger: log,
|
|
httpServer: &http.Server{},
|
|
loggingEnabled: loggingEnabled,
|
|
debugMode: debugMode,
|
|
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()
|
|
|
|
// 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 && (bytecode == nil || 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()
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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 {
|
|
// 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)
|
|
}
|
|
}
|