237 lines
6.2 KiB
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)
|
|
}
|
|
}
|