// server.go - Simplified HTTP server package http import ( "bytes" "context" "strings" "sync" "time" "Moonshark/router" "Moonshark/runner" "Moonshark/sessions" "Moonshark/utils" "Moonshark/utils/color" "Moonshark/utils/config" "Moonshark/utils/logger" "Moonshark/utils/metadata" "github.com/valyala/fasthttp" ) var ( debugPath = []byte("/debug/stats") staticMethods = map[string]bool{"GET": true, "HEAD": true, "OPTIONS": true} cached404, cached500 []byte cacheMu sync.RWMutex emptyMap = make(map[string]any) ) type Server struct { luaRouter *router.Router staticHandler fasthttp.RequestHandler luaRunner *runner.Runner fasthttpServer *fasthttp.Server sessionManager *sessions.SessionManager cfg *config.Config debugMode bool staticPrefixBytes []byte } func New(luaRouter *router.Router, runner *runner.Runner, cfg *config.Config, debugMode bool) *Server { staticPrefix := cfg.Server.StaticPrefix if !strings.HasPrefix(staticPrefix, "/") { staticPrefix = "/" + staticPrefix } if !strings.HasSuffix(staticPrefix, "/") { staticPrefix += "/" } s := &Server{ luaRouter: luaRouter, luaRunner: runner, debugMode: debugMode, cfg: cfg, sessionManager: sessions.GlobalSessionManager, staticPrefixBytes: []byte(staticPrefix), } // Cache error pages errorConfig := utils.ErrorPageConfig{ OverrideDir: cfg.Dirs.Override, DebugMode: debugMode, } cacheMu.Lock() cached404 = []byte(utils.NotFoundPage(errorConfig, "")) cached500 = []byte(utils.InternalErrorPage(errorConfig, "", "Internal Server Error")) cacheMu.Unlock() // Setup static file serving if cfg.Dirs.Static != "" { staticFS := &fasthttp.FS{ Root: cfg.Dirs.Static, IndexNames: []string{"index.html"}, AcceptByteRange: true, Compress: true, CompressedFileSuffix: ".gz", CompressBrotli: true, PathRewrite: fasthttp.NewPathPrefixStripper(len(staticPrefix) - 1), } s.staticHandler = staticFS.NewRequestHandler() } s.fasthttpServer = &fasthttp.Server{ Handler: s.handleRequest, Name: "Moonshark/" + metadata.Version, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 120 * time.Second, MaxRequestBodySize: 16 << 20, TCPKeepalive: true, ReduceMemoryUsage: true, StreamRequestBody: true, NoDefaultServerHeader: true, } return s } func (s *Server) ListenAndServe(addr string) error { logger.Infof("Catch the swell at %s", color.Cyan("http://localhost"+addr)) 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()) // Debug stats endpoint if s.debugMode && bytes.Equal(ctx.Path(), debugPath) { s.handleDebugStats(ctx) s.logRequest(ctx, method, path, time.Since(start)) return } // Static file serving if s.staticHandler != nil && bytes.HasPrefix(ctx.Path(), s.staticPrefixBytes) { s.staticHandler(ctx) s.logRequest(ctx, method, path, time.Since(start)) return } // Route lookup bytecode, params, found := s.luaRouter.Lookup(method, path) if !found { s.send404(ctx) s.logRequest(ctx, method, path, time.Since(start)) return } if len(bytecode) == 0 { s.send500(ctx, nil) s.logRequest(ctx, method, path, time.Since(start)) return } // Get session session := s.sessionManager.GetSessionFromRequest(ctx) // Execute Lua script response, err := s.luaRunner.ExecuteHTTP(bytecode, ctx, params, session) if err != nil { logger.Errorf("Lua execution error: %v", err) s.send500(ctx, err) s.logRequest(ctx, method, path, time.Since(start)) return } // Apply response s.applyResponse(ctx, response, session) runner.ReleaseResponse(response) s.logRequest(ctx, method, path, time.Since(start)) } func (s *Server) applyResponse(ctx *fasthttp.RequestCtx, resp *runner.Response, session *sessions.Session) { // Handle session updates if len(resp.SessionData) > 0 { if _, clearAll := resp.SessionData["__clear_all"]; clearAll { session.Clear() session.ClearFlash() delete(resp.SessionData, "__clear_all") } for k, v := range resp.SessionData { if v == "__DELETE__" { session.Delete(k) } else { session.Set(k, v) } } } // Handle flash data if flashData, ok := resp.Metadata["flash"].(map[string]any); ok { for k, v := range flashData { if err := session.FlashSafe(k, v); err != nil && s.debugMode { logger.Warnf("Error setting flash data %s: %v", k, err) } } } // Apply session cookie s.sessionManager.ApplySessionCookie(ctx, session) // Apply HTTP response runner.ApplyResponse(resp, ctx) } func (s *Server) send404(ctx *fasthttp.RequestCtx) { ctx.SetContentType("text/html; charset=utf-8") ctx.SetStatusCode(fasthttp.StatusNotFound) cacheMu.RLock() ctx.SetBody(cached404) cacheMu.RUnlock() } func (s *Server) send500(ctx *fasthttp.RequestCtx, err error) { ctx.SetContentType("text/html; charset=utf-8") ctx.SetStatusCode(fasthttp.StatusInternalServerError) if err == nil { cacheMu.RLock() ctx.SetBody(cached500) cacheMu.RUnlock() } else { errorConfig := utils.ErrorPageConfig{ OverrideDir: s.cfg.Dirs.Override, DebugMode: s.debugMode, } ctx.SetBody([]byte(utils.InternalErrorPage(errorConfig, string(ctx.Path()), err.Error()))) } } func (s *Server) handleDebugStats(ctx *fasthttp.RequestCtx) { stats := utils.CollectSystemStats(s.cfg) stats.Components = utils.ComponentStats{ RouteCount: 0, // TODO: Get from router BytecodeBytes: 0, // TODO: Get from router SessionStats: s.sessionManager.GetCacheStats(), } ctx.SetContentType("text/html; charset=utf-8") ctx.SetStatusCode(fasthttp.StatusOK) ctx.SetBody([]byte(utils.DebugStatsPage(stats))) } func (s *Server) logRequest(ctx *fasthttp.RequestCtx, method, path string, duration time.Duration) { if s.cfg.Server.HTTPLogging { logger.Request(ctx.Response.StatusCode(), method, path, duration) } }