Moonshark/http/server.go
2025-05-26 13:03:29 -05:00

212 lines
5.9 KiB
Go

package http
import (
"context"
"sync"
"time"
"Moonshark/routers"
"Moonshark/runner"
"Moonshark/sessions"
"Moonshark/utils"
"Moonshark/utils/color"
"Moonshark/utils/config"
"Moonshark/utils/logger"
"Moonshark/utils/metadata"
"github.com/valyala/fasthttp"
)
type Server struct {
luaRouter *routers.LuaRouter
staticRouter *routers.StaticRouter
luaRunner *runner.Runner
fasthttpServer *fasthttp.Server
loggingEnabled bool
debugMode bool
config *config.Config
sessionManager *sessions.SessionManager
errorConfig utils.ErrorPageConfig
ctxPool sync.Pool
}
func New(luaRouter *routers.LuaRouter, staticRouter *routers.StaticRouter,
runner *runner.Runner, loggingEnabled bool, debugMode bool,
overrideDir string, config *config.Config) *Server {
s := &Server{
luaRouter: luaRouter,
staticRouter: staticRouter,
luaRunner: runner,
loggingEnabled: loggingEnabled,
debugMode: debugMode,
config: config,
sessionManager: sessions.GlobalSessionManager,
errorConfig: utils.ErrorPageConfig{
OverrideDir: overrideDir,
DebugMode: debugMode,
},
ctxPool: sync.Pool{
New: func() any {
return make(map[string]any, 8)
},
},
}
s.fasthttpServer = &fasthttp.Server{
Handler: s.handleRequest,
Name: "Moonshark/" + metadata.Version,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
MaxRequestBodySize: 16 << 20,
TCPKeepalive: true,
TCPKeepalivePeriod: 60 * time.Second,
ReduceMemoryUsage: true,
DisablePreParseMultipartForm: true,
}
return s
}
func (s *Server) ListenAndServe(addr string) error {
logger.Info("Catch the swell at %s", color.Apply("http://localhost"+addr, color.Cyan))
return s.fasthttpServer.ListenAndServe(addr)
}
func (s *Server) Shutdown(ctx context.Context) error {
return s.fasthttpServer.ShutdownWithContext(ctx)
}
func (s *Server) handleRequest(ctx *fasthttp.RequestCtx) {
start := time.Now()
method := string(ctx.Method())
path := string(ctx.Path())
if s.debugMode && path == "/debug/stats" {
s.handleDebugStats(ctx)
if s.loggingEnabled {
logger.LogRequest(ctx.Response.StatusCode(), method, path, time.Since(start))
}
return
}
logger.Debug("Processing request %s %s", method, path)
params := &routers.Params{}
bytecode, scriptPath, routeErr, found := s.luaRouter.GetRouteInfo(method, path, params)
if found {
if len(bytecode) == 0 || routeErr != nil {
errorMsg := "Route exists but failed to compile. Check server logs for details."
if routeErr != nil {
errorMsg = routeErr.Error()
}
logger.Error("%s %s - %s", method, path, errorMsg)
ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, path, errorMsg)))
} else {
logger.Debug("Found Lua route match for %s %s with %d params", method, path, params.Count)
s.handleLuaRoute(ctx, bytecode, scriptPath, params, method, path)
}
} else if s.staticRouter != nil {
if _, found := s.staticRouter.Match(path); found {
s.staticRouter.ServeHTTP(ctx)
} else {
ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusNotFound)
ctx.SetBody([]byte(utils.NotFoundPage(s.errorConfig, path)))
}
} else {
ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusNotFound)
ctx.SetBody([]byte(utils.NotFoundPage(s.errorConfig, path)))
}
if s.loggingEnabled {
logger.LogRequest(ctx.Response.StatusCode(), method, path, time.Since(start))
}
}
func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scriptPath string, params *routers.Params, method, path string) {
luaCtx := runner.NewHTTPContext(ctx)
defer luaCtx.Release()
sessionMap := s.ctxPool.Get().(map[string]any)
defer func() {
for k := range sessionMap {
delete(sessionMap, k)
}
s.ctxPool.Put(sessionMap)
}()
session := s.sessionManager.GetSessionFromRequest(ctx)
sessionMap["id"] = session.ID
sessionMap["data"] = session.GetAll()
luaCtx.Set("method", method)
luaCtx.Set("path", path)
luaCtx.Set("host", string(ctx.Host()))
luaCtx.Set("session", sessionMap)
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]
}
luaCtx.Set("params", paramMap)
} else {
luaCtx.Set("params", emptyMap)
}
if method == "POST" || method == "PUT" || method == "PATCH" {
if formData, err := ParseForm(ctx); err == nil {
luaCtx.Set("form", formData)
} else {
logger.Warning("Error parsing form: %v", err)
luaCtx.Set("form", emptyMap)
}
} else {
luaCtx.Set("form", emptyMap)
}
response, err := s.luaRunner.Run(bytecode, luaCtx, scriptPath)
if err != nil {
logger.Error("Error executing Lua route: %v", err)
ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, path, err.Error())))
return
}
if _, clearAll := response.SessionData["__clear_all"]; clearAll {
session.Clear()
delete(response.SessionData, "__clear_all")
}
for k, v := range response.SessionData {
if v == "__SESSION_DELETE_MARKER__" {
session.Delete(k)
} else {
session.Set(k, v)
}
}
s.sessionManager.ApplySessionCookie(ctx, session)
runner.ApplyResponse(response, ctx)
runner.ReleaseResponse(response)
}
func (s *Server) handleDebugStats(ctx *fasthttp.RequestCtx) {
stats := utils.CollectSystemStats(s.config)
routeCount, bytecodeBytes := s.luaRouter.GetRouteStats()
stats.Components = utils.ComponentStats{
RouteCount: routeCount,
BytecodeBytes: bytecodeBytes,
SessionStats: sessions.GlobalSessionManager.GetCacheStats(),
}
ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusOK)
ctx.SetBody([]byte(utils.DebugStatsPage(stats)))
}