345 lines
9.2 KiB
Go
345 lines
9.2 KiB
Go
package http
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"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 (
|
|
//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
|
|
loggingEnabled bool
|
|
debugMode bool
|
|
config *config.Config
|
|
sessionManager *sessions.SessionManager
|
|
errorConfig utils.ErrorPageConfig
|
|
ctxPool sync.Pool
|
|
paramsPool sync.Pool
|
|
staticDir string
|
|
staticPrefix string
|
|
staticPrefixBytes []byte
|
|
|
|
// Cached error pages
|
|
cached404 []byte
|
|
cached500 []byte
|
|
errorCacheMu sync.RWMutex
|
|
}
|
|
|
|
func New(luaRouter *router.LuaRouter, staticDir string,
|
|
runner *runner.Runner, loggingEnabled bool, debugMode bool,
|
|
overrideDir string, config *config.Config) *Server {
|
|
|
|
staticPrefix := config.Server.StaticPrefix
|
|
if staticPrefix == "" {
|
|
staticPrefix = "/static/"
|
|
}
|
|
|
|
if staticPrefix[0] != '/' {
|
|
staticPrefix = "/" + staticPrefix
|
|
}
|
|
if staticPrefix[len(staticPrefix)-1] != '/' {
|
|
staticPrefix = staticPrefix + "/"
|
|
}
|
|
|
|
s := &Server{
|
|
luaRouter: luaRouter,
|
|
luaRunner: runner,
|
|
loggingEnabled: loggingEnabled,
|
|
debugMode: debugMode,
|
|
config: config,
|
|
sessionManager: sessions.GlobalSessionManager,
|
|
staticDir: staticDir,
|
|
staticPrefix: staticPrefix,
|
|
staticPrefixBytes: []byte(staticPrefix),
|
|
errorConfig: utils.ErrorPageConfig{
|
|
OverrideDir: overrideDir,
|
|
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 staticDir != "" {
|
|
s.staticFS = &fasthttp.FS{
|
|
Root: staticDir,
|
|
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.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()
|
|
methodBytes := ctx.Method()
|
|
pathBytes := ctx.Path()
|
|
|
|
// Fast path for debug stats
|
|
if s.debugMode && bytes.Equal(pathBytes, debugPath) {
|
|
s.handleDebugStats(ctx)
|
|
if s.loggingEnabled {
|
|
logger.LogRequest(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start))
|
|
}
|
|
return
|
|
}
|
|
|
|
// Fast path for static files
|
|
if s.staticHandler != nil && bytes.HasPrefix(pathBytes, s.staticPrefixBytes) {
|
|
s.staticHandler(ctx)
|
|
if s.loggingEnabled {
|
|
logger.LogRequest(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start))
|
|
}
|
|
return
|
|
}
|
|
|
|
// Lua route lookup - only allocate params if found
|
|
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.loggingEnabled {
|
|
logger.LogRequest(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 runner.GetGlobalEnvManager() != nil {
|
|
luaCtx.Set("env", runner.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.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("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.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)))
|
|
}
|
|
|
|
// 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()
|
|
}
|