Moonshark/http/server.go

333 lines
9.0 KiB
Go

package http
import (
"bytes"
"context"
"strings"
"sync"
"time"
"Moonshark/router"
"Moonshark/runner"
"Moonshark/runner/lualibs"
"Moonshark/sessions"
"Moonshark/utils"
"Moonshark/utils/color"
"Moonshark/utils/config"
"Moonshark/utils/logger"
"Moonshark/utils/metadata"
"github.com/valyala/fasthttp"
)
var (
//methodGET = []byte("GET")
methodPOST = []byte("POST")
methodPUT = []byte("PUT")
methodPATCH = []byte("PATCH")
debugPath = []byte("/debug/stats")
)
type Server struct {
luaRouter *router.LuaRouter
staticHandler fasthttp.RequestHandler
staticFS *fasthttp.FS
luaRunner *runner.Runner
fasthttpServer *fasthttp.Server
debugMode bool
cfg *config.Config
sessionManager *sessions.SessionManager
errorConfig utils.ErrorPageConfig
ctxPool sync.Pool
paramsPool sync.Pool
staticPrefix string
staticPrefixBytes []byte
// Cached error pages
cached404 []byte
cached500 []byte
errorCacheMu sync.RWMutex
}
func New(luaRouter *router.LuaRouter, runner *runner.Runner, cfg *config.Config, debugMode bool) *Server {
staticPrefix := cfg.Server.StaticPrefix
if !strings.HasPrefix(staticPrefix, "/") {
staticPrefix = "/" + staticPrefix
}
if !strings.HasSuffix(staticPrefix, "/") {
staticPrefix = staticPrefix + "/"
}
s := &Server{
luaRouter: luaRouter,
luaRunner: runner,
debugMode: debugMode,
cfg: cfg,
sessionManager: sessions.GlobalSessionManager,
staticPrefix: staticPrefix,
staticPrefixBytes: []byte(staticPrefix),
errorConfig: utils.ErrorPageConfig{
OverrideDir: cfg.Dirs.Override,
DebugMode: debugMode,
},
ctxPool: sync.Pool{
New: func() any {
return make(map[string]any, 6)
},
},
paramsPool: sync.Pool{
New: func() any {
return make(map[string]any, 4)
},
},
}
// Pre-cache error pages
s.cached404 = []byte(utils.NotFoundPage(s.errorConfig, ""))
s.cached500 = []byte(utils.InternalErrorPage(s.errorConfig, "", "Internal Server Error"))
// Setup static file serving
if cfg.Dirs.Static != "" {
s.staticFS = &fasthttp.FS{
Root: cfg.Dirs.Static,
IndexNames: []string{"index.html"},
GenerateIndexPages: false,
AcceptByteRange: true,
Compress: true,
CompressedFileSuffix: ".gz",
CompressBrotli: true,
CompressZstd: true,
PathRewrite: fasthttp.NewPathPrefixStripper(len(staticPrefix) - 1),
}
s.staticHandler = s.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,
TCPKeepalivePeriod: 60 * time.Second,
ReduceMemoryUsage: true,
DisablePreParseMultipartForm: true,
DisableHeaderNamesNormalizing: true,
NoDefaultServerHeader: true,
StreamRequestBody: 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()
methodBytes := ctx.Method()
pathBytes := ctx.Path()
if s.debugMode && bytes.Equal(pathBytes, debugPath) {
s.handleDebugStats(ctx)
if s.cfg.Server.HTTPLogging {
logger.Request(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start))
}
return
}
if s.staticHandler != nil && bytes.HasPrefix(pathBytes, s.staticPrefixBytes) {
s.staticHandler(ctx)
if s.cfg.Server.HTTPLogging {
logger.Request(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start))
}
return
}
bytecode, scriptPath, routeErr, params, found := s.luaRouter.GetRouteInfo(methodBytes, pathBytes)
if found {
if len(bytecode) == 0 || routeErr != nil {
s.sendError(ctx, fasthttp.StatusInternalServerError, pathBytes, routeErr)
} else {
s.handleLuaRoute(ctx, bytecode, scriptPath, params, methodBytes, pathBytes)
}
} else {
s.send404(ctx, pathBytes)
}
if s.cfg.Server.HTTPLogging {
logger.Request(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start))
}
}
func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scriptPath string,
params *router.Params, methodBytes, pathBytes []byte) {
luaCtx := runner.NewHTTPContext(ctx)
defer luaCtx.Release()
if lualibs.GetGlobalEnvManager() != nil {
luaCtx.Set("env", lualibs.GetGlobalEnvManager().GetAll())
}
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
// Only get session data if not empty
if !session.IsEmpty() {
sessionMap["data"] = session.GetAll()
} else {
sessionMap["data"] = emptyMap
}
// Set basic context
luaCtx.Set("method", string(methodBytes))
luaCtx.Set("path", string(pathBytes))
luaCtx.Set("host", string(ctx.Host()))
luaCtx.Set("session", sessionMap)
// Add headers to context
headers := make(map[string]any)
ctx.Request.Header.VisitAll(func(key, value []byte) {
headers[string(key)] = string(value)
})
luaCtx.Set("headers", headers)
// Handle params
if params != nil && params.Count > 0 {
paramMap := s.paramsPool.Get().(map[string]any)
for i := range params.Count {
paramMap[params.Keys[i]] = params.Values[i]
}
luaCtx.Set("params", paramMap)
defer func() {
for k := range paramMap {
delete(paramMap, k)
}
s.paramsPool.Put(paramMap)
}()
} else {
luaCtx.Set("params", emptyMap)
}
// Parse form data for POST/PUT/PATCH
if bytes.Equal(methodBytes, methodPOST) ||
bytes.Equal(methodBytes, methodPUT) ||
bytes.Equal(methodBytes, methodPATCH) {
if formData, err := ParseForm(ctx); err == nil {
luaCtx.Set("form", formData)
} else {
if s.debugMode {
logger.Warnf("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.Errorf("Lua execution error: %v", err)
s.sendError(ctx, fasthttp.StatusInternalServerError, pathBytes, err)
return
}
// Handle session updates
if len(response.SessionData) > 0 {
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) send404(ctx *fasthttp.RequestCtx, pathBytes []byte) {
ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusNotFound)
// Use cached 404 for common case
if len(pathBytes) == 1 && pathBytes[0] == '/' {
s.errorCacheMu.RLock()
ctx.SetBody(s.cached404)
s.errorCacheMu.RUnlock()
} else {
ctx.SetBody([]byte(utils.NotFoundPage(s.errorConfig, string(pathBytes))))
}
}
func (s *Server) sendError(ctx *fasthttp.RequestCtx, status int, pathBytes []byte, err error) {
ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(status)
if err == nil {
s.errorCacheMu.RLock()
ctx.SetBody(s.cached500)
s.errorCacheMu.RUnlock()
} else {
ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, string(pathBytes), err.Error())))
}
}
func (s *Server) handleDebugStats(ctx *fasthttp.RequestCtx) {
stats := utils.CollectSystemStats(s.cfg)
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)))
}
// SetStaticCaching enables/disables static file caching
func (s *Server) SetStaticCaching(duration time.Duration) {
if s.staticFS != nil {
s.staticFS.CacheDuration = duration
s.staticHandler = s.staticFS.NewRequestHandler()
}
}
// GetStaticPrefix returns the URL prefix for static files
func (s *Server) GetStaticPrefix() string {
return s.staticPrefix
}
// UpdateErrorCache refreshes cached error pages
func (s *Server) UpdateErrorCache() {
s.errorCacheMu.Lock()
s.cached404 = []byte(utils.NotFoundPage(s.errorConfig, ""))
s.cached500 = []byte(utils.InternalErrorPage(s.errorConfig, "", "Internal Server Error"))
s.errorCacheMu.Unlock()
}