348 lines
9.5 KiB
Go
348 lines
9.5 KiB
Go
package http
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"Moonshark/core/metadata"
|
|
"Moonshark/core/routers"
|
|
"Moonshark/core/runner"
|
|
"Moonshark/core/utils"
|
|
"Moonshark/core/utils/config"
|
|
"Moonshark/core/utils/logger"
|
|
|
|
"github.com/goccy/go-json"
|
|
"github.com/valyala/fasthttp"
|
|
)
|
|
|
|
// Server handles HTTP requests using Lua and static file routers
|
|
type Server struct {
|
|
luaRouter *routers.LuaRouter
|
|
staticRouter *routers.StaticRouter
|
|
luaRunner *runner.Runner
|
|
fasthttpServer *fasthttp.Server
|
|
loggingEnabled bool
|
|
debugMode bool
|
|
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,
|
|
loggingEnabled: loggingEnabled,
|
|
debugMode: debugMode,
|
|
config: config,
|
|
errorConfig: utils.ErrorPageConfig{
|
|
OverrideDir: overrideDir,
|
|
DebugMode: debugMode,
|
|
},
|
|
}
|
|
|
|
// Configure fasthttp server
|
|
server.fasthttpServer = &fasthttp.Server{
|
|
Handler: server.handleRequest,
|
|
Name: "Moonshark/" + metadata.Version,
|
|
ReadTimeout: 30 * time.Second,
|
|
WriteTimeout: 30 * time.Second,
|
|
MaxRequestBodySize: 16 << 20, // 16MB - consistent with Forms.go
|
|
DisableKeepalive: false,
|
|
TCPKeepalive: true,
|
|
TCPKeepalivePeriod: 60 * time.Second,
|
|
ReduceMemoryUsage: true,
|
|
GetOnly: false,
|
|
DisablePreParseMultipartForm: true, // We'll handle parsing manually
|
|
}
|
|
|
|
return server
|
|
}
|
|
|
|
// ListenAndServe starts the server on the given address
|
|
func (s *Server) ListenAndServe(addr string) error {
|
|
logger.ServerCont("Catch the swell at http://localhost%s", addr)
|
|
return s.fasthttpServer.ListenAndServe(addr)
|
|
}
|
|
|
|
// Shutdown gracefully shuts down the server
|
|
func (s *Server) Shutdown(ctx context.Context) error {
|
|
return s.fasthttpServer.ShutdownWithContext(ctx)
|
|
}
|
|
|
|
// handleRequest processes the HTTP request
|
|
func (s *Server) handleRequest(ctx *fasthttp.RequestCtx) {
|
|
start := time.Now()
|
|
method := string(ctx.Method())
|
|
path := string(ctx.Path())
|
|
|
|
// Special case for debug stats when debug mode is enabled
|
|
if s.debugMode && path == "/debug/stats" {
|
|
s.handleDebugStats(ctx)
|
|
|
|
// Log request
|
|
if s.loggingEnabled {
|
|
duration := time.Since(start)
|
|
LogRequest(ctx.Response.StatusCode(), method, path, duration)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Process the request
|
|
s.processRequest(ctx)
|
|
|
|
// Log the request with our custom format
|
|
if s.loggingEnabled {
|
|
duration := time.Since(start)
|
|
LogRequest(ctx.Response.StatusCode(), method, path, duration)
|
|
}
|
|
}
|
|
|
|
// processRequest processes the actual request
|
|
func (s *Server) processRequest(ctx *fasthttp.RequestCtx) {
|
|
method := string(ctx.Method())
|
|
path := string(ctx.Path())
|
|
|
|
logger.Debug("Processing request %s %s", method, path)
|
|
|
|
// Try Lua routes first
|
|
params := &routers.Params{}
|
|
bytecode, scriptPath, found := s.luaRouter.GetBytecode(method, 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
|
|
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(method, path, params); node != nil && node.Error != nil {
|
|
errorMsg = node.Error.Error()
|
|
}
|
|
|
|
logger.Error("%s %s - %s", method, path, errorMsg)
|
|
|
|
// Show error page with the actual error message
|
|
ctx.SetContentType("text/html; charset=utf-8")
|
|
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
|
|
errorHTML := utils.InternalErrorPage(s.errorConfig, path, errorMsg)
|
|
ctx.SetBody([]byte(errorHTML))
|
|
return
|
|
} else if found {
|
|
logger.Debug("Found Lua route match for %s %s with %d params", method, path, params.Count)
|
|
s.handleLuaRoute(ctx, bytecode, scriptPath, params)
|
|
return
|
|
}
|
|
|
|
// Then try static files
|
|
if _, found := s.staticRouter.Match(path); found {
|
|
s.staticRouter.ServeHTTP(ctx)
|
|
return
|
|
}
|
|
|
|
// No route found - 404 Not Found
|
|
ctx.SetContentType("text/html; charset=utf-8")
|
|
ctx.SetStatusCode(fasthttp.StatusNotFound)
|
|
ctx.SetBody([]byte(utils.NotFoundPage(s.errorConfig, path)))
|
|
}
|
|
|
|
// HandleMethodNotAllowed responds with a 405 Method Not Allowed error
|
|
func HandleMethodNotAllowed(ctx *fasthttp.RequestCtx, errorConfig utils.ErrorPageConfig) {
|
|
path := string(ctx.Path())
|
|
ctx.SetContentType("text/html; charset=utf-8")
|
|
ctx.SetStatusCode(fasthttp.StatusMethodNotAllowed)
|
|
ctx.SetBody([]byte(utils.MethodNotAllowedPage(errorConfig, path)))
|
|
}
|
|
|
|
// handleLuaRoute executes a Lua route
|
|
func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scriptPath string, params *routers.Params) {
|
|
luaCtx := runner.NewContext()
|
|
defer luaCtx.Release()
|
|
|
|
method := string(ctx.Method())
|
|
path := string(ctx.Path())
|
|
host := string(ctx.Host())
|
|
|
|
// Set up context
|
|
luaCtx.Set("method", method)
|
|
luaCtx.Set("path", path)
|
|
luaCtx.Set("host", host)
|
|
|
|
// Headers
|
|
headerMap := make(map[string]any)
|
|
ctx.Request.Header.VisitAll(func(key, value []byte) {
|
|
headerMap[string(key)] = string(value)
|
|
})
|
|
luaCtx.Set("headers", headerMap)
|
|
|
|
// Cookies
|
|
cookieMap := make(map[string]any)
|
|
ctx.Request.Header.VisitAllCookie(func(key, value []byte) {
|
|
cookieMap[string(key)] = string(value)
|
|
})
|
|
if len(cookieMap) > 0 {
|
|
luaCtx.Set("cookies", cookieMap)
|
|
luaCtx.Set("_request_cookies", cookieMap) // For backward compatibility
|
|
} else {
|
|
luaCtx.Set("cookies", make(map[string]any))
|
|
luaCtx.Set("_request_cookies", make(map[string]any))
|
|
}
|
|
|
|
// URL parameters
|
|
if params.Count > 0 {
|
|
paramMap := make(map[string]any, params.Count)
|
|
for i, key := range params.Keys {
|
|
paramMap[key] = params.Values[i]
|
|
}
|
|
luaCtx.Set("params", paramMap)
|
|
} else {
|
|
luaCtx.Set("params", make(map[string]any))
|
|
}
|
|
|
|
// Query parameters
|
|
queryMap := QueryToLua(ctx)
|
|
luaCtx.Set("query", queryMap)
|
|
|
|
// Form data
|
|
if method == "POST" || method == "PUT" || method == "PATCH" {
|
|
formData, err := ParseForm(ctx)
|
|
if err == nil && len(formData) > 0 {
|
|
luaCtx.Set("form", formData)
|
|
} else if err != nil {
|
|
logger.Warning("Error parsing form: %v", err)
|
|
luaCtx.Set("form", make(map[string]any))
|
|
} else {
|
|
luaCtx.Set("form", make(map[string]any))
|
|
}
|
|
} else {
|
|
luaCtx.Set("form", make(map[string]any))
|
|
}
|
|
|
|
// Execute Lua script
|
|
result, err := s.luaRunner.Run(bytecode, luaCtx, 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(ctx, s.errorConfig)
|
|
return
|
|
}
|
|
|
|
// Normal error handling
|
|
logger.Error("Error executing Lua route: %v", err)
|
|
ctx.SetContentType("text/html; charset=utf-8")
|
|
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
|
|
errorHTML := utils.InternalErrorPage(s.errorConfig, path, err.Error())
|
|
ctx.SetBody([]byte(errorHTML))
|
|
return
|
|
}
|
|
|
|
writeResponse(ctx, result)
|
|
}
|
|
|
|
// Content types for responses
|
|
const (
|
|
contentTypeJSON = "application/json"
|
|
contentTypePlain = "text/plain"
|
|
)
|
|
|
|
// writeResponse writes the Lua result to the HTTP response
|
|
func writeResponse(ctx *fasthttp.RequestCtx, result any) {
|
|
if result == nil {
|
|
ctx.SetStatusCode(fasthttp.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 {
|
|
ctx.Response.Header.Set(name, value)
|
|
}
|
|
|
|
// Set cookies
|
|
for _, cookie := range httpResp.Cookies {
|
|
ctx.Response.Header.SetCookie(cookie)
|
|
}
|
|
|
|
// Set status code
|
|
ctx.SetStatusCode(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(ctx, contentTypeJSON)
|
|
data, err := json.Marshal(result)
|
|
if err != nil {
|
|
logger.Error("Failed to marshal response: %v", err)
|
|
ctx.Error("Internal Server Error", fasthttp.StatusInternalServerError)
|
|
return
|
|
}
|
|
ctx.SetBody(data)
|
|
return
|
|
}
|
|
|
|
// All other types - convert to plain text
|
|
setContentTypeIfMissing(ctx, contentTypePlain)
|
|
|
|
switch r := result.(type) {
|
|
case string:
|
|
ctx.SetBodyString(r)
|
|
case []byte:
|
|
ctx.SetBody(r)
|
|
default:
|
|
// Convert any other type to string
|
|
ctx.SetBodyString(fmt.Sprintf("%v", r))
|
|
}
|
|
}
|
|
|
|
func setContentTypeIfMissing(ctx *fasthttp.RequestCtx, contentType string) {
|
|
if len(ctx.Response.Header.ContentType()) == 0 {
|
|
ctx.SetContentType(contentType)
|
|
}
|
|
}
|
|
|
|
// handleDebugStats displays debug statistics
|
|
func (s *Server) handleDebugStats(ctx *fasthttp.RequestCtx) {
|
|
// 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
|
|
ctx.SetContentType("text/html; charset=utf-8")
|
|
ctx.SetStatusCode(fasthttp.StatusOK)
|
|
ctx.SetBody([]byte(html))
|
|
}
|