Moonshark/http/server.go

237 lines
6.2 KiB
Go

// server.go - Simplified HTTP server
package http
import (
"bytes"
"context"
"strings"
"sync"
"time"
"Moonshark/color"
"Moonshark/router"
"Moonshark/runner"
"Moonshark/sessions"
"Moonshark/utils"
"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)
}
}