restart
This commit is contained in:
parent
843e318e01
commit
f75ba90f74
5
.gitignore
vendored
5
.gitignore
vendored
@ -23,7 +23,4 @@ luajit/.git
|
|||||||
go.work
|
go.work
|
||||||
|
|
||||||
# Test directories and files
|
# Test directories and files
|
||||||
/config.lua
|
test.lua
|
||||||
test/
|
|
||||||
/init.lua
|
|
||||||
/moonshark
|
|
||||||
|
19
go.mod
19
go.mod
@ -1,22 +1,3 @@
|
|||||||
module Moonshark
|
module Moonshark
|
||||||
|
|
||||||
go 1.24.1
|
go 1.24.1
|
||||||
|
|
||||||
require (
|
|
||||||
git.sharkk.net/Sky/LuaJIT-to-Go v0.5.5
|
|
||||||
github.com/VictoriaMetrics/fastcache v1.12.5
|
|
||||||
github.com/deneonet/benc v1.1.8
|
|
||||||
github.com/goccy/go-json v0.10.5
|
|
||||||
github.com/matoous/go-nanoid/v2 v2.1.0
|
|
||||||
github.com/valyala/fasthttp v1.63.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
|
||||||
github.com/golang/snappy v1.0.0 // indirect
|
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
|
||||||
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect
|
|
||||||
golang.org/x/sys v0.34.0 // indirect
|
|
||||||
)
|
|
||||||
|
38
go.sum
38
go.sum
@ -1,38 +0,0 @@
|
|||||||
git.sharkk.net/Sky/LuaJIT-to-Go v0.5.5 h1:AoJCKReryVnJBTcrbb9Xqq41rKH4XkDrsFZHGgpW5fY=
|
|
||||||
git.sharkk.net/Sky/LuaJIT-to-Go v0.5.5/go.mod h1:HQz+D7AFxOfNbTIogjxP+shEBtz1KKrLlLucU+w07c8=
|
|
||||||
github.com/VictoriaMetrics/fastcache v1.12.5 h1:966OX9JjqYmDAFdp3wEXLwzukiHIm+GVlZHv6B8KW3k=
|
|
||||||
github.com/VictoriaMetrics/fastcache v1.12.5/go.mod h1:K+JGPBn0sueFlLjZ8rcVM0cKkWKNElKyQXmw57QOoYI=
|
|
||||||
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
|
|
||||||
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
|
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/deneonet/benc v1.1.8 h1:Qk9diyH0UcnduvCrZ62mBrwUeSZzte4kQxMbclVdhW4=
|
|
||||||
github.com/deneonet/benc v1.1.8/go.mod h1:UCfkM5Od0B2huwv/ZItvtUb7QnALFt9YXtX8NXX4Lts=
|
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
|
||||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
|
||||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
|
||||||
github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE=
|
|
||||||
github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
|
||||||
github.com/valyala/fasthttp v1.63.0 h1:DisIL8OjB7ul2d7cBaMRcKTQDYnrGy56R4FCiuDP0Ns=
|
|
||||||
github.com/valyala/fasthttp v1.63.0/go.mod h1:REc4IeW+cAEyLrRPa5A81MIjvz0QE1laoTX2EaPHKJM=
|
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
|
||||||
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc=
|
|
||||||
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
|
|
||||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
|
||||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
597
http/http.go
597
http/http.go
@ -1,597 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
_ "embed"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"mime/multipart"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"Moonshark/http/router"
|
|
||||||
"Moonshark/http/sessions"
|
|
||||||
|
|
||||||
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
|
|
||||||
"github.com/goccy/go-json"
|
|
||||||
"github.com/valyala/fasthttp"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed http.lua
|
|
||||||
var httpLuaCode string
|
|
||||||
|
|
||||||
// HandlerFunc represents a Lua handler
|
|
||||||
type HandlerFunc struct {
|
|
||||||
bytecode []byte
|
|
||||||
funcRef int
|
|
||||||
name string
|
|
||||||
isFunction bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server with single state for function handler compatibility
|
|
||||||
type Server struct {
|
|
||||||
server *fasthttp.Server
|
|
||||||
router *router.Router
|
|
||||||
sessions *sessions.SessionManager
|
|
||||||
state *luajit.State
|
|
||||||
stateMu sync.Mutex
|
|
||||||
handlers map[int]*HandlerFunc
|
|
||||||
handlersMu sync.RWMutex
|
|
||||||
funcCounter int
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestContext with lazy parsing
|
|
||||||
type RequestContext struct {
|
|
||||||
ctx *fasthttp.RequestCtx
|
|
||||||
params *router.Params
|
|
||||||
session *sessions.Session
|
|
||||||
parsedForm map[string]any
|
|
||||||
formOnce sync.Once
|
|
||||||
}
|
|
||||||
|
|
||||||
var globalServer *Server
|
|
||||||
|
|
||||||
func NewServer(state *luajit.State) *Server {
|
|
||||||
return &Server{
|
|
||||||
router: router.New(),
|
|
||||||
sessions: sessions.NewSessionManager(10000),
|
|
||||||
state: state,
|
|
||||||
handlers: make(map[int]*HandlerFunc),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func RegisterHTTPFunctions(L *luajit.State) error {
|
|
||||||
globalServer = NewServer(L)
|
|
||||||
|
|
||||||
functions := map[string]luajit.GoFunction{
|
|
||||||
"__http_listen": globalServer.httpListen,
|
|
||||||
"__http_route": globalServer.httpRoute,
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, fn := range functions {
|
|
||||||
if err := L.RegisterGoFunction(name, fn); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return L.DoString(httpLuaCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) httpListen(state *luajit.State) int {
|
|
||||||
port, err := state.SafeToNumber(1)
|
|
||||||
if err != nil {
|
|
||||||
return state.PushError("listen: port must be number")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.server = &fasthttp.Server{
|
|
||||||
Handler: s.fastRequestHandler,
|
|
||||||
Name: "Moonshark/2.0",
|
|
||||||
Concurrency: 256 * 1024,
|
|
||||||
ReadBufferSize: 4096,
|
|
||||||
WriteBufferSize: 4096,
|
|
||||||
ReduceMemoryUsage: true,
|
|
||||||
NoDefaultServerHeader: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
addr := fmt.Sprintf(":%d", int(port))
|
|
||||||
go func() {
|
|
||||||
if err := s.server.ListenAndServe(addr); err != nil {
|
|
||||||
fmt.Printf("Server error: %v\n", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
fmt.Printf("Server listening on port %d\n", int(port))
|
|
||||||
state.PushBoolean(true)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) httpRoute(state *luajit.State) int {
|
|
||||||
method, err := state.SafeToString(1)
|
|
||||||
if err != nil {
|
|
||||||
return state.PushError("route: method must be string")
|
|
||||||
}
|
|
||||||
|
|
||||||
path, err := state.SafeToString(2)
|
|
||||||
if err != nil {
|
|
||||||
return state.PushError("route: path must be string")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.funcCounter++
|
|
||||||
handlerID := s.funcCounter
|
|
||||||
|
|
||||||
if state.IsFunction(3) {
|
|
||||||
// Function handler - store reference
|
|
||||||
state.PushCopy(3)
|
|
||||||
funcRef := s.storeFunction(state)
|
|
||||||
|
|
||||||
s.handlersMu.Lock()
|
|
||||||
s.handlers[handlerID] = &HandlerFunc{
|
|
||||||
funcRef: funcRef,
|
|
||||||
name: fmt.Sprintf("%s %s", method, path),
|
|
||||||
isFunction: true,
|
|
||||||
}
|
|
||||||
s.handlersMu.Unlock()
|
|
||||||
} else {
|
|
||||||
// String handler - compile to bytecode
|
|
||||||
handlerCode, err := state.SafeToString(3)
|
|
||||||
if err != nil {
|
|
||||||
return state.PushError("route: handler must be function or string")
|
|
||||||
}
|
|
||||||
|
|
||||||
bytecode, err := state.CompileBytecode(handlerCode, fmt.Sprintf("handler_%s_%s", method, path))
|
|
||||||
if err != nil {
|
|
||||||
return state.PushError("route: failed to compile handler: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
s.handlersMu.Lock()
|
|
||||||
s.handlers[handlerID] = &HandlerFunc{
|
|
||||||
bytecode: bytecode,
|
|
||||||
name: fmt.Sprintf("%s %s", method, path),
|
|
||||||
isFunction: false,
|
|
||||||
}
|
|
||||||
s.handlersMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add route to router
|
|
||||||
if err := s.router.AddRoute(strings.ToUpper(method), path, handlerID); err != nil {
|
|
||||||
return state.PushError("route: failed to add route: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
state.PushBoolean(true)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) storeFunction(state *luajit.State) int {
|
|
||||||
state.GetGlobal("__moonshark_functions")
|
|
||||||
if state.IsNil(-1) {
|
|
||||||
state.Pop(1)
|
|
||||||
state.NewTable()
|
|
||||||
state.PushCopy(-1)
|
|
||||||
state.SetGlobal("__moonshark_functions")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.funcCounter++
|
|
||||||
state.PushNumber(float64(s.funcCounter))
|
|
||||||
state.PushCopy(-3)
|
|
||||||
state.SetTable(-3)
|
|
||||||
state.Pop(2)
|
|
||||||
|
|
||||||
return s.funcCounter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) getFunction(state *luajit.State, ref int) bool {
|
|
||||||
state.GetGlobal("__moonshark_functions")
|
|
||||||
if state.IsNil(-1) {
|
|
||||||
state.Pop(1)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
state.PushNumber(float64(ref))
|
|
||||||
state.GetTable(-2)
|
|
||||||
isFunc := state.IsFunction(-1)
|
|
||||||
if !isFunc {
|
|
||||||
state.Pop(2)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
state.Remove(-2)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) fastRequestHandler(ctx *fasthttp.RequestCtx) {
|
|
||||||
method := string(ctx.Method())
|
|
||||||
path := string(ctx.Path())
|
|
||||||
|
|
||||||
// Fast route lookup
|
|
||||||
handlerID, params, found := s.router.Lookup(method, path)
|
|
||||||
if !found {
|
|
||||||
ctx.SetStatusCode(404)
|
|
||||||
ctx.SetBodyString("Not Found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get compiled handler
|
|
||||||
s.handlersMu.RLock()
|
|
||||||
handler := s.handlers[handlerID]
|
|
||||||
s.handlersMu.RUnlock()
|
|
||||||
|
|
||||||
if handler == nil {
|
|
||||||
ctx.SetStatusCode(500)
|
|
||||||
ctx.SetBodyString("Handler not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lock state for execution
|
|
||||||
s.stateMu.Lock()
|
|
||||||
defer s.stateMu.Unlock()
|
|
||||||
|
|
||||||
// Setup request context
|
|
||||||
reqCtx := &RequestContext{
|
|
||||||
ctx: ctx,
|
|
||||||
params: params,
|
|
||||||
session: s.sessions.GetSessionFromRequest(ctx),
|
|
||||||
}
|
|
||||||
reqCtx.session.AdvanceFlash()
|
|
||||||
|
|
||||||
var responseBody string
|
|
||||||
|
|
||||||
if handler.isFunction {
|
|
||||||
// Function handler - use traditional approach
|
|
||||||
s.setupFunctionEnvironment(s.state, reqCtx)
|
|
||||||
|
|
||||||
if !s.getFunction(s.state, handler.funcRef) {
|
|
||||||
ctx.SetStatusCode(500)
|
|
||||||
ctx.SetBodyString("Function handler not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push request object
|
|
||||||
if err := s.state.PushValue(s.requestToTable(reqCtx)); err != nil {
|
|
||||||
ctx.SetStatusCode(500)
|
|
||||||
ctx.SetBodyString("Failed to create request object")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.state.Call(1, 1); err != nil {
|
|
||||||
ctx.SetStatusCode(500)
|
|
||||||
ctx.SetBodyString(fmt.Sprintf("Handler error: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.state.GetTop() > 0 && !s.state.IsNil(-1) {
|
|
||||||
responseBody = s.state.ToString(-1)
|
|
||||||
}
|
|
||||||
s.state.Pop(1)
|
|
||||||
} else {
|
|
||||||
// Bytecode handler - use fast approach
|
|
||||||
s.setupFastEnvironment(s.state, reqCtx)
|
|
||||||
|
|
||||||
if err := s.state.LoadAndRunBytecode(handler.bytecode, handler.name); err != nil {
|
|
||||||
ctx.SetStatusCode(500)
|
|
||||||
ctx.SetBodyString(fmt.Sprintf("Handler error: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.state.GetTop() > 0 && !s.state.IsNil(-1) {
|
|
||||||
responseBody = s.state.ToString(-1)
|
|
||||||
}
|
|
||||||
s.state.Pop(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply response
|
|
||||||
s.applyResponse(ctx, s.state, responseBody, reqCtx.session)
|
|
||||||
s.sessions.ApplySessionCookie(ctx, reqCtx.session)
|
|
||||||
|
|
||||||
// Clean up state
|
|
||||||
s.state.SetTop(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) setupFunctionEnvironment(state *luajit.State, reqCtx *RequestContext) {
|
|
||||||
// Set up response globals for function handlers
|
|
||||||
state.NewTable()
|
|
||||||
state.PushNumber(200)
|
|
||||||
state.SetField(-2, "status")
|
|
||||||
state.NewTable()
|
|
||||||
state.SetField(-2, "headers")
|
|
||||||
state.NewTable()
|
|
||||||
state.SetField(-2, "cookies")
|
|
||||||
state.SetGlobal("__response")
|
|
||||||
|
|
||||||
// Session data
|
|
||||||
if !reqCtx.session.IsEmpty() {
|
|
||||||
state.PushValue(reqCtx.session.GetAll())
|
|
||||||
state.SetGlobal("__session")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) setupFastEnvironment(state *luajit.State, reqCtx *RequestContext) {
|
|
||||||
// Request basics as globals for fast access
|
|
||||||
state.PushString(string(reqCtx.ctx.Method()))
|
|
||||||
state.SetGlobal("REQUEST_METHOD")
|
|
||||||
|
|
||||||
state.PushString(string(reqCtx.ctx.Path()))
|
|
||||||
state.SetGlobal("REQUEST_PATH")
|
|
||||||
|
|
||||||
// Parameters
|
|
||||||
if reqCtx.params != nil && len(reqCtx.params.Keys) > 0 {
|
|
||||||
paramMap := make(map[string]string, len(reqCtx.params.Keys))
|
|
||||||
for i, key := range reqCtx.params.Keys {
|
|
||||||
if i < len(reqCtx.params.Values) {
|
|
||||||
paramMap[key] = reqCtx.params.Values[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.PushValue(paramMap)
|
|
||||||
state.SetGlobal("PARAMS")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query parameters
|
|
||||||
queryMap := make(map[string]string)
|
|
||||||
reqCtx.ctx.QueryArgs().VisitAll(func(key, value []byte) {
|
|
||||||
queryMap[string(key)] = string(value)
|
|
||||||
})
|
|
||||||
if len(queryMap) > 0 {
|
|
||||||
state.PushValue(queryMap)
|
|
||||||
state.SetGlobal("QUERY")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Headers
|
|
||||||
headerMap := make(map[string]string)
|
|
||||||
reqCtx.ctx.Request.Header.VisitAll(func(key, value []byte) {
|
|
||||||
headerMap[string(key)] = string(value)
|
|
||||||
})
|
|
||||||
state.PushValue(headerMap)
|
|
||||||
state.SetGlobal("HEADERS")
|
|
||||||
|
|
||||||
// Cookies
|
|
||||||
cookieMap := make(map[string]string)
|
|
||||||
reqCtx.ctx.Request.Header.VisitAllCookie(func(key, value []byte) {
|
|
||||||
cookieMap[string(key)] = string(value)
|
|
||||||
})
|
|
||||||
if len(cookieMap) > 0 {
|
|
||||||
state.PushValue(cookieMap)
|
|
||||||
state.SetGlobal("COOKIES")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Form data
|
|
||||||
if reqCtx.ctx.IsPost() || reqCtx.ctx.IsPut() || reqCtx.ctx.IsPatch() {
|
|
||||||
form := s.parseForm(reqCtx.ctx)
|
|
||||||
if len(form) > 0 {
|
|
||||||
state.PushValue(form)
|
|
||||||
state.SetGlobal("FORM")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Session data
|
|
||||||
if !reqCtx.session.IsEmpty() {
|
|
||||||
state.PushValue(reqCtx.session.GetAll())
|
|
||||||
state.SetGlobal("session_data")
|
|
||||||
}
|
|
||||||
|
|
||||||
// CSRF token
|
|
||||||
if csrfToken := s.generateCSRFToken(); csrfToken != "" {
|
|
||||||
state.PushString(csrfToken)
|
|
||||||
state.SetGlobal("CSRF_TOKEN")
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSON encode fallback
|
|
||||||
state.RegisterGoFunction("json_encode_fallback", func(state *luajit.State) int {
|
|
||||||
val, _ := state.ToValue(1)
|
|
||||||
if b, err := json.Marshal(val); err == nil {
|
|
||||||
state.PushString(string(b))
|
|
||||||
} else {
|
|
||||||
state.PushString("null")
|
|
||||||
}
|
|
||||||
return 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) requestToTable(reqCtx *RequestContext) map[string]any {
|
|
||||||
req := map[string]any{
|
|
||||||
"method": string(reqCtx.ctx.Method()),
|
|
||||||
"path": string(reqCtx.ctx.Path()),
|
|
||||||
"headers": make(map[string]string),
|
|
||||||
"query": make(map[string]string),
|
|
||||||
"cookies": make(map[string]string),
|
|
||||||
"body": string(reqCtx.ctx.PostBody()),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Headers
|
|
||||||
headers := req["headers"].(map[string]string)
|
|
||||||
reqCtx.ctx.Request.Header.VisitAll(func(key, value []byte) {
|
|
||||||
headers[string(key)] = string(value)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Cookies
|
|
||||||
cookies := req["cookies"].(map[string]string)
|
|
||||||
reqCtx.ctx.Request.Header.VisitAllCookie(func(key, value []byte) {
|
|
||||||
cookies[string(key)] = string(value)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Query
|
|
||||||
query := req["query"].(map[string]string)
|
|
||||||
reqCtx.ctx.QueryArgs().VisitAll(func(key, value []byte) {
|
|
||||||
query[string(key)] = string(value)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Params
|
|
||||||
if reqCtx.params != nil && len(reqCtx.params.Keys) > 0 {
|
|
||||||
params := make(map[string]string, len(reqCtx.params.Keys))
|
|
||||||
for i, key := range reqCtx.params.Keys {
|
|
||||||
if i < len(reqCtx.params.Values) {
|
|
||||||
params[key] = reqCtx.params.Values[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
req["params"] = params
|
|
||||||
}
|
|
||||||
|
|
||||||
// Form
|
|
||||||
if reqCtx.ctx.IsPost() || reqCtx.ctx.IsPut() || reqCtx.ctx.IsPatch() {
|
|
||||||
req["form"] = s.parseForm(reqCtx.ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
return req
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) applyResponse(ctx *fasthttp.RequestCtx, state *luajit.State, body string, session *sessions.Session) {
|
|
||||||
// Update session from Lua
|
|
||||||
state.GetGlobal("session_data")
|
|
||||||
if state.IsTable(-1) {
|
|
||||||
if data, err := state.ToTable(-1); err == nil {
|
|
||||||
if dataMap, ok := data.(map[string]any); ok {
|
|
||||||
session.Clear()
|
|
||||||
for k, v := range dataMap {
|
|
||||||
session.Set(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.Pop(1)
|
|
||||||
|
|
||||||
// Check for response table (function handlers) or response global (fast handlers)
|
|
||||||
state.GetGlobal("__response")
|
|
||||||
if state.IsNil(-1) {
|
|
||||||
state.Pop(1)
|
|
||||||
state.GetGlobal("response")
|
|
||||||
if state.IsNil(-1) {
|
|
||||||
state.Pop(1)
|
|
||||||
if body != "" {
|
|
||||||
ctx.SetBodyString(body)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status
|
|
||||||
if status := state.GetFieldNumber(-1, "status", 200); status != 200 {
|
|
||||||
ctx.SetStatusCode(int(status))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Headers
|
|
||||||
state.GetField(-1, "headers")
|
|
||||||
if state.IsTable(-1) {
|
|
||||||
state.ForEachTableKV(-1, func(key, value string) bool {
|
|
||||||
ctx.Response.Header.Set(key, value)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
state.Pop(1)
|
|
||||||
|
|
||||||
// Cookies
|
|
||||||
state.GetField(-1, "cookies")
|
|
||||||
if state.IsTable(-1) {
|
|
||||||
s.applyCookies(ctx, state)
|
|
||||||
}
|
|
||||||
state.Pop(1)
|
|
||||||
|
|
||||||
state.Pop(1)
|
|
||||||
|
|
||||||
if body != "" {
|
|
||||||
ctx.SetBodyString(body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) applyCookies(ctx *fasthttp.RequestCtx, state *luajit.State) {
|
|
||||||
state.ForEachArray(-1, func(i int, st *luajit.State) bool {
|
|
||||||
if !st.IsTable(-1) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
name := st.GetFieldString(-1, "name", "")
|
|
||||||
value := st.GetFieldString(-1, "value", "")
|
|
||||||
if name == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
cookie := fasthttp.AcquireCookie()
|
|
||||||
defer fasthttp.ReleaseCookie(cookie)
|
|
||||||
|
|
||||||
cookie.SetKey(name)
|
|
||||||
cookie.SetValue(value)
|
|
||||||
|
|
||||||
if options, ok := st.GetFieldTable(-1, "options"); ok {
|
|
||||||
if optMap, ok := options.(map[string]any); ok {
|
|
||||||
if path, ok := optMap["path"].(string); ok {
|
|
||||||
cookie.SetPath(path)
|
|
||||||
} else {
|
|
||||||
cookie.SetPath("/")
|
|
||||||
}
|
|
||||||
if domain, ok := optMap["domain"].(string); ok {
|
|
||||||
cookie.SetDomain(domain)
|
|
||||||
}
|
|
||||||
if secure, ok := optMap["secure"].(bool); ok && secure {
|
|
||||||
cookie.SetSecure(true)
|
|
||||||
}
|
|
||||||
if httpOnly, ok := optMap["http_only"].(bool); ok {
|
|
||||||
cookie.SetHTTPOnly(httpOnly)
|
|
||||||
} else {
|
|
||||||
cookie.SetHTTPOnly(true)
|
|
||||||
}
|
|
||||||
if maxAge, ok := optMap["max_age"].(int); ok && maxAge > 0 {
|
|
||||||
cookie.SetExpire(time.Now().Add(time.Duration(maxAge) * time.Second))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Response.Header.SetCookie(cookie)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) parseForm(ctx *fasthttp.RequestCtx) map[string]any {
|
|
||||||
contentType := string(ctx.Request.Header.ContentType())
|
|
||||||
form := make(map[string]any)
|
|
||||||
|
|
||||||
if strings.Contains(contentType, "application/json") {
|
|
||||||
var data map[string]any
|
|
||||||
if err := json.Unmarshal(ctx.PostBody(), &data); err == nil {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
} else if strings.Contains(contentType, "application/x-www-form-urlencoded") {
|
|
||||||
ctx.PostArgs().VisitAll(func(key, value []byte) {
|
|
||||||
form[string(key)] = string(value)
|
|
||||||
})
|
|
||||||
} else if strings.Contains(contentType, "multipart/form-data") {
|
|
||||||
if multipartForm, err := ctx.MultipartForm(); err == nil {
|
|
||||||
for key, values := range multipartForm.Value {
|
|
||||||
if len(values) == 1 {
|
|
||||||
form[key] = values[0]
|
|
||||||
} else {
|
|
||||||
form[key] = values
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(multipartForm.File) > 0 {
|
|
||||||
files := make(map[string]any)
|
|
||||||
for fieldName, fileHeaders := range multipartForm.File {
|
|
||||||
if len(fileHeaders) == 1 {
|
|
||||||
files[fieldName] = s.fileToMap(fileHeaders[0])
|
|
||||||
} else {
|
|
||||||
fileList := make([]map[string]any, len(fileHeaders))
|
|
||||||
for i, fh := range fileHeaders {
|
|
||||||
fileList[i] = s.fileToMap(fh)
|
|
||||||
}
|
|
||||||
files[fieldName] = fileList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
form["_files"] = files
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return form
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) fileToMap(fh *multipart.FileHeader) map[string]any {
|
|
||||||
return map[string]any{
|
|
||||||
"filename": fh.Filename,
|
|
||||||
"size": fh.Size,
|
|
||||||
"mimetype": fh.Header.Get("Content-Type"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) generateCSRFToken() string {
|
|
||||||
bytes := make([]byte, 32)
|
|
||||||
rand.Read(bytes)
|
|
||||||
return base64.URLEncoding.EncodeToString(bytes)
|
|
||||||
}
|
|
149
http/http.lua
149
http/http.lua
@ -1,149 +0,0 @@
|
|||||||
-- Fast response handling
|
|
||||||
local response = {status = 200, headers = {}, cookies = {}}
|
|
||||||
local session_data = {}
|
|
||||||
|
|
||||||
http = {}
|
|
||||||
|
|
||||||
function http.listen(port)
|
|
||||||
return __http_listen(port)
|
|
||||||
end
|
|
||||||
|
|
||||||
function http.route(method, path, handler)
|
|
||||||
return __http_route(method, path, handler)
|
|
||||||
end
|
|
||||||
|
|
||||||
function http.status(code)
|
|
||||||
response.status = code
|
|
||||||
end
|
|
||||||
|
|
||||||
function http.header(k, v)
|
|
||||||
response.headers[k] = v
|
|
||||||
end
|
|
||||||
|
|
||||||
function http.json(data)
|
|
||||||
http.header("Content-Type", "application/json")
|
|
||||||
return json.encode(data)
|
|
||||||
end
|
|
||||||
|
|
||||||
function http.html(content)
|
|
||||||
http.header("Content-Type", "text/html")
|
|
||||||
return content
|
|
||||||
end
|
|
||||||
|
|
||||||
function http.text(content)
|
|
||||||
http.header("Content-Type", "text/plain")
|
|
||||||
return content
|
|
||||||
end
|
|
||||||
|
|
||||||
function http.redirect(url, code)
|
|
||||||
response.status = code or 302
|
|
||||||
response.headers["Location"] = url
|
|
||||||
coroutine.yield()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Session functions
|
|
||||||
session = {}
|
|
||||||
|
|
||||||
function session.get(key)
|
|
||||||
return session_data[key]
|
|
||||||
end
|
|
||||||
|
|
||||||
function session.set(key, val)
|
|
||||||
session_data[key] = val
|
|
||||||
end
|
|
||||||
|
|
||||||
function session.flash(key, val)
|
|
||||||
session_data["_flash_" .. key] = val
|
|
||||||
end
|
|
||||||
|
|
||||||
function session.get_flash(key)
|
|
||||||
local val = session_data["_flash_" .. key]
|
|
||||||
session_data["_flash_" .. key] = nil
|
|
||||||
return val
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Cookie functions
|
|
||||||
cookie = {}
|
|
||||||
|
|
||||||
function cookie.get(name)
|
|
||||||
return COOKIES and COOKIES[name]
|
|
||||||
end
|
|
||||||
|
|
||||||
function cookie.set(name, value, options)
|
|
||||||
response.cookies[#response.cookies + 1] = {
|
|
||||||
name = name,
|
|
||||||
value = value,
|
|
||||||
options = options or {}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- CSRF functions
|
|
||||||
csrf = {}
|
|
||||||
|
|
||||||
function csrf.generate()
|
|
||||||
local token = CSRF_TOKEN or ""
|
|
||||||
session.set("_csrf_token", token)
|
|
||||||
return token
|
|
||||||
end
|
|
||||||
|
|
||||||
function csrf.validate()
|
|
||||||
local session_token = session.get("_csrf_token")
|
|
||||||
local form_token = FORM and FORM._csrf_token
|
|
||||||
return session_token and session_token == form_token
|
|
||||||
end
|
|
||||||
|
|
||||||
function csrf.field()
|
|
||||||
return '<input type="hidden" name="_csrf_token" value="' .. csrf.generate() .. '" />'
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Fast JSON encoding
|
|
||||||
json = {
|
|
||||||
encode = function(data)
|
|
||||||
if type(data) == "string" then
|
|
||||||
return '"' .. data .. '"'
|
|
||||||
elseif type(data) == "number" then
|
|
||||||
return tostring(data)
|
|
||||||
elseif type(data) == "boolean" then
|
|
||||||
return data and "true" or "false"
|
|
||||||
elseif data == nil then
|
|
||||||
return "null"
|
|
||||||
elseif type(data) == "table" then
|
|
||||||
-- Check if it's an array
|
|
||||||
local isArray = true
|
|
||||||
local n = 0
|
|
||||||
for k, v in pairs(data) do
|
|
||||||
n = n + 1
|
|
||||||
if type(k) ~= "number" or k ~= n then
|
|
||||||
isArray = false
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if isArray then
|
|
||||||
local result = "["
|
|
||||||
for i = 1, n do
|
|
||||||
if i > 1 then result = result .. "," end
|
|
||||||
result = result .. json.encode(data[i])
|
|
||||||
end
|
|
||||||
return result .. "]"
|
|
||||||
else
|
|
||||||
local result = "{"
|
|
||||||
local first = true
|
|
||||||
for k, v in pairs(data) do
|
|
||||||
if not first then result = result .. "," end
|
|
||||||
result = result .. '"' .. tostring(k) .. '":' .. json.encode(v)
|
|
||||||
first = false
|
|
||||||
end
|
|
||||||
return result .. "}"
|
|
||||||
end
|
|
||||||
else
|
|
||||||
return json_encode_fallback(data)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Helper functions
|
|
||||||
function redirect_with_flash(url, type, message)
|
|
||||||
session.flash(type, message)
|
|
||||||
http.redirect(url)
|
|
||||||
end
|
|
@ -1,241 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handler function that takes parameters as strings
|
|
||||||
type Handler func(params []string)
|
|
||||||
|
|
||||||
type node struct {
|
|
||||||
segment string
|
|
||||||
handlerID int
|
|
||||||
children []*node
|
|
||||||
isDynamic bool
|
|
||||||
isWildcard bool
|
|
||||||
maxParams uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
type Router struct {
|
|
||||||
get, post, put, patch, delete *node
|
|
||||||
paramsBuffer []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Params holds URL parameters
|
|
||||||
type Params struct {
|
|
||||||
Keys []string
|
|
||||||
Values []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns a parameter value by name
|
|
||||||
func (p *Params) Get(name string) string {
|
|
||||||
for i, key := range p.Keys {
|
|
||||||
if key == name && i < len(p.Values) {
|
|
||||||
return p.Values[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new Router instance
|
|
||||||
func New() *Router {
|
|
||||||
return &Router{
|
|
||||||
get: &node{},
|
|
||||||
post: &node{},
|
|
||||||
put: &node{},
|
|
||||||
patch: &node{},
|
|
||||||
delete: &node{},
|
|
||||||
paramsBuffer: make([]string, 64),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) methodNode(method string) *node {
|
|
||||||
switch method {
|
|
||||||
case "GET":
|
|
||||||
return r.get
|
|
||||||
case "POST":
|
|
||||||
return r.post
|
|
||||||
case "PUT":
|
|
||||||
return r.put
|
|
||||||
case "PATCH":
|
|
||||||
return r.patch
|
|
||||||
case "DELETE":
|
|
||||||
return r.delete
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddRoute adds a route with handler ID (for compatibility)
|
|
||||||
func (r *Router) AddRoute(method, path string, handlerID int) error {
|
|
||||||
root := r.methodNode(method)
|
|
||||||
if root == nil {
|
|
||||||
return fmt.Errorf("unsupported method: %s", method)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a handler that stores the ID
|
|
||||||
h := func(params []string) {
|
|
||||||
// This is a placeholder - the actual execution happens in HTTP module
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.addRoute(root, path, h, handlerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// readSegment extracts the next path segment
|
|
||||||
func readSegment(path string, start int) (segment string, end int, hasMore bool) {
|
|
||||||
if start >= len(path) {
|
|
||||||
return "", start, false
|
|
||||||
}
|
|
||||||
if path[start] == '/' {
|
|
||||||
start++
|
|
||||||
}
|
|
||||||
if start >= len(path) {
|
|
||||||
return "", start, false
|
|
||||||
}
|
|
||||||
end = start
|
|
||||||
for end < len(path) && path[end] != '/' {
|
|
||||||
end++
|
|
||||||
}
|
|
||||||
return path[start:end], end, end < len(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// addRoute adds a new route to the trie
|
|
||||||
func (r *Router) addRoute(root *node, path string, h Handler, handlerID int) error {
|
|
||||||
if path == "/" {
|
|
||||||
root.handlerID = handlerID
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
current := root
|
|
||||||
pos := 0
|
|
||||||
lastWC := false
|
|
||||||
count := uint8(0)
|
|
||||||
|
|
||||||
for {
|
|
||||||
seg, newPos, more := readSegment(path, pos)
|
|
||||||
if seg == "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
isDyn := len(seg) > 1 && seg[0] == ':'
|
|
||||||
isWC := len(seg) > 0 && seg[0] == '*'
|
|
||||||
|
|
||||||
if isWC {
|
|
||||||
if lastWC || more {
|
|
||||||
return fmt.Errorf("wildcard must be the last segment in the path")
|
|
||||||
}
|
|
||||||
lastWC = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if isDyn || isWC {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
|
|
||||||
var child *node
|
|
||||||
for _, c := range current.children {
|
|
||||||
if c.segment == seg {
|
|
||||||
child = c
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if child == nil {
|
|
||||||
child = &node{segment: seg, isDynamic: isDyn, isWildcard: isWC}
|
|
||||||
current.children = append(current.children, child)
|
|
||||||
}
|
|
||||||
|
|
||||||
if child.maxParams < count {
|
|
||||||
child.maxParams = count
|
|
||||||
}
|
|
||||||
|
|
||||||
current = child
|
|
||||||
pos = newPos
|
|
||||||
}
|
|
||||||
|
|
||||||
current.handlerID = handlerID
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lookup finds a handler matching method and path
|
|
||||||
func (r *Router) Lookup(method, path string) (int, *Params, bool) {
|
|
||||||
root := r.methodNode(method)
|
|
||||||
if root == nil {
|
|
||||||
return 0, nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
if path == "/" {
|
|
||||||
if root.handlerID != 0 {
|
|
||||||
return root.handlerID, &Params{}, true
|
|
||||||
}
|
|
||||||
return 0, nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer := r.paramsBuffer
|
|
||||||
if cap(buffer) < int(root.maxParams) {
|
|
||||||
buffer = make([]string, root.maxParams)
|
|
||||||
r.paramsBuffer = buffer
|
|
||||||
}
|
|
||||||
buffer = buffer[:0]
|
|
||||||
|
|
||||||
handlerID, paramCount, paramKeys, found := r.match(root, path, 0, &buffer)
|
|
||||||
if !found {
|
|
||||||
return 0, nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
params := &Params{
|
|
||||||
Keys: paramKeys,
|
|
||||||
Values: buffer[:paramCount],
|
|
||||||
}
|
|
||||||
|
|
||||||
return handlerID, params, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// match traverses the trie to find a handler
|
|
||||||
func (r *Router) match(current *node, path string, start int, params *[]string) (int, int, []string, bool) {
|
|
||||||
paramCount := 0
|
|
||||||
var paramKeys []string
|
|
||||||
|
|
||||||
// Check wildcards first
|
|
||||||
for _, c := range current.children {
|
|
||||||
if c.isWildcard {
|
|
||||||
rem := path[start:]
|
|
||||||
if len(rem) > 0 && rem[0] == '/' {
|
|
||||||
rem = rem[1:]
|
|
||||||
}
|
|
||||||
*params = append(*params, rem)
|
|
||||||
// Extract param name from *name format
|
|
||||||
paramName := c.segment[1:] // Remove the * prefix
|
|
||||||
paramKeys = append(paramKeys, paramName)
|
|
||||||
return c.handlerID, 1, paramKeys, c.handlerID != 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
seg, pos, more := readSegment(path, start)
|
|
||||||
if seg == "" {
|
|
||||||
return current.handlerID, 0, paramKeys, current.handlerID != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range current.children {
|
|
||||||
if c.segment == seg || c.isDynamic {
|
|
||||||
if c.isDynamic {
|
|
||||||
*params = append(*params, seg)
|
|
||||||
// Extract param name from :name format
|
|
||||||
paramName := c.segment[1:] // Remove the : prefix
|
|
||||||
paramKeys = append(paramKeys, paramName)
|
|
||||||
paramCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
if !more {
|
|
||||||
return c.handlerID, paramCount, paramKeys, c.handlerID != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
handlerID, nestedCount, nestedKeys, ok := r.match(c, path, pos, params)
|
|
||||||
if ok {
|
|
||||||
allKeys := append(paramKeys, nestedKeys...)
|
|
||||||
return handlerID, paramCount + nestedCount, allKeys, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, 0, nil, false
|
|
||||||
}
|
|
@ -1,211 +0,0 @@
|
|||||||
package sessions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/fastcache"
|
|
||||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
|
||||||
"github.com/valyala/fasthttp"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
DefaultMaxSessions = 10000
|
|
||||||
DefaultCookieName = "MoonsharkSID"
|
|
||||||
DefaultCookiePath = "/"
|
|
||||||
DefaultMaxAge = 86400 // 1 day in seconds
|
|
||||||
CleanupInterval = 5 * time.Minute
|
|
||||||
)
|
|
||||||
|
|
||||||
// SessionManager handles multiple sessions
|
|
||||||
type SessionManager struct {
|
|
||||||
cache *fastcache.Cache
|
|
||||||
cookieName string
|
|
||||||
cookiePath string
|
|
||||||
cookieDomain string
|
|
||||||
cookieSecure bool
|
|
||||||
cookieHTTPOnly bool
|
|
||||||
cookieMaxAge int
|
|
||||||
cookieMu sync.RWMutex
|
|
||||||
cleanupTicker *time.Ticker
|
|
||||||
cleanupDone chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSessionManager creates a new session manager
|
|
||||||
func NewSessionManager(maxSessions int) *SessionManager {
|
|
||||||
if maxSessions <= 0 {
|
|
||||||
maxSessions = DefaultMaxSessions
|
|
||||||
}
|
|
||||||
|
|
||||||
sm := &SessionManager{
|
|
||||||
cache: fastcache.New(maxSessions * 4096),
|
|
||||||
cookieName: DefaultCookieName,
|
|
||||||
cookiePath: DefaultCookiePath,
|
|
||||||
cookieDomain: "",
|
|
||||||
cookieSecure: false,
|
|
||||||
cookieHTTPOnly: true,
|
|
||||||
cookieMaxAge: DefaultMaxAge,
|
|
||||||
cleanupDone: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pre-populate session pool
|
|
||||||
for range 100 {
|
|
||||||
s := NewSession("", 0)
|
|
||||||
s.Release()
|
|
||||||
}
|
|
||||||
|
|
||||||
sm.cleanupTicker = time.NewTicker(CleanupInterval)
|
|
||||||
go sm.cleanupRoutine()
|
|
||||||
|
|
||||||
return sm
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop shuts down the session manager's cleanup routine
|
|
||||||
func (sm *SessionManager) Stop() {
|
|
||||||
close(sm.cleanupDone)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sm *SessionManager) cleanupRoutine() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-sm.cleanupTicker.C:
|
|
||||||
sm.CleanupExpired()
|
|
||||||
case <-sm.cleanupDone:
|
|
||||||
sm.cleanupTicker.Stop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSession retrieves a session by ID, or creates a new one if it doesn't exist
|
|
||||||
func (sm *SessionManager) GetSession(id string) *Session {
|
|
||||||
if id != "" {
|
|
||||||
if data := sm.cache.Get(nil, []byte(id)); len(data) > 0 {
|
|
||||||
if s, err := Unmarshal(data); err == nil && !s.IsExpired() {
|
|
||||||
s.UpdateLastUsed()
|
|
||||||
s.ResetDirty()
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
sm.cache.Del([]byte(id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sm.CreateSession()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateSession generates a new session with a unique ID
|
|
||||||
func (sm *SessionManager) CreateSession() *Session {
|
|
||||||
id, _ := gonanoid.New()
|
|
||||||
|
|
||||||
// Ensure uniqueness (max 3 attempts)
|
|
||||||
for i := 0; i < 3 && sm.cache.Has([]byte(id)); i++ {
|
|
||||||
id, _ = gonanoid.New()
|
|
||||||
}
|
|
||||||
|
|
||||||
s := NewSession(id, sm.cookieMaxAge)
|
|
||||||
if data, err := s.Marshal(); err == nil {
|
|
||||||
sm.cache.Set([]byte(id), data)
|
|
||||||
}
|
|
||||||
s.ResetDirty()
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// DestroySession removes a session
|
|
||||||
func (sm *SessionManager) DestroySession(id string) {
|
|
||||||
if data := sm.cache.Get(nil, []byte(id)); len(data) > 0 {
|
|
||||||
if s, err := Unmarshal(data); err == nil {
|
|
||||||
s.Release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sm.cache.Del([]byte(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
// CleanupExpired removes all expired sessions
|
|
||||||
func (sm *SessionManager) CleanupExpired() int {
|
|
||||||
// fastcache doesn't support iteration
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCookieOptions configures cookie parameters
|
|
||||||
func (sm *SessionManager) SetCookieOptions(name, path, domain string, secure, httpOnly bool, maxAge int) {
|
|
||||||
sm.cookieMu.Lock()
|
|
||||||
sm.cookieName = name
|
|
||||||
sm.cookiePath = path
|
|
||||||
sm.cookieDomain = domain
|
|
||||||
sm.cookieSecure = secure
|
|
||||||
sm.cookieHTTPOnly = httpOnly
|
|
||||||
sm.cookieMaxAge = maxAge
|
|
||||||
sm.cookieMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSessionFromRequest extracts the session from a request
|
|
||||||
func (sm *SessionManager) GetSessionFromRequest(ctx *fasthttp.RequestCtx) *Session {
|
|
||||||
sm.cookieMu.RLock()
|
|
||||||
name := sm.cookieName
|
|
||||||
sm.cookieMu.RUnlock()
|
|
||||||
|
|
||||||
if cookie := ctx.Request.Header.Cookie(name); len(cookie) > 0 {
|
|
||||||
return sm.GetSession(string(cookie))
|
|
||||||
}
|
|
||||||
return sm.CreateSession()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ApplySessionCookie adds the session cookie to the response
|
|
||||||
func (sm *SessionManager) ApplySessionCookie(ctx *fasthttp.RequestCtx, session *Session) {
|
|
||||||
if session.IsDirty() {
|
|
||||||
if data, err := session.Marshal(); err == nil {
|
|
||||||
sm.cache.Set([]byte(session.ID), data)
|
|
||||||
}
|
|
||||||
session.ResetDirty()
|
|
||||||
}
|
|
||||||
|
|
||||||
cookie := fasthttp.AcquireCookie()
|
|
||||||
defer fasthttp.ReleaseCookie(cookie)
|
|
||||||
|
|
||||||
sm.cookieMu.RLock()
|
|
||||||
cookie.SetKey(sm.cookieName)
|
|
||||||
cookie.SetPath(sm.cookiePath)
|
|
||||||
cookie.SetHTTPOnly(sm.cookieHTTPOnly)
|
|
||||||
cookie.SetMaxAge(sm.cookieMaxAge)
|
|
||||||
if sm.cookieDomain != "" {
|
|
||||||
cookie.SetDomain(sm.cookieDomain)
|
|
||||||
}
|
|
||||||
cookie.SetSecure(sm.cookieSecure)
|
|
||||||
sm.cookieMu.RUnlock()
|
|
||||||
|
|
||||||
cookie.SetValue(session.ID)
|
|
||||||
ctx.Response.Header.SetCookie(cookie)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CookieOptions returns the cookie options for this session manager
|
|
||||||
func (sm *SessionManager) CookieOptions() map[string]any {
|
|
||||||
sm.cookieMu.RLock()
|
|
||||||
defer sm.cookieMu.RUnlock()
|
|
||||||
|
|
||||||
return map[string]any{
|
|
||||||
"name": sm.cookieName,
|
|
||||||
"path": sm.cookiePath,
|
|
||||||
"domain": sm.cookieDomain,
|
|
||||||
"secure": sm.cookieSecure,
|
|
||||||
"http_only": sm.cookieHTTPOnly,
|
|
||||||
"max_age": sm.cookieMaxAge,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCacheStats returns statistics about the session cache
|
|
||||||
func (sm *SessionManager) GetCacheStats() map[string]uint64 {
|
|
||||||
if sm == nil || sm.cache == nil {
|
|
||||||
return map[string]uint64{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var stats fastcache.Stats
|
|
||||||
sm.cache.UpdateStats(&stats)
|
|
||||||
|
|
||||||
return map[string]uint64{
|
|
||||||
"entries": stats.EntriesCount,
|
|
||||||
"bytes": stats.BytesSize,
|
|
||||||
"max_bytes": stats.MaxBytesSize,
|
|
||||||
"gets": stats.GetCalls,
|
|
||||||
"sets": stats.SetCalls,
|
|
||||||
"misses": stats.Misses,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,552 +0,0 @@
|
|||||||
package sessions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/deneonet/benc"
|
|
||||||
bstd "github.com/deneonet/benc/std"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Session stores data for a single user session
|
|
||||||
type Session struct {
|
|
||||||
ID string
|
|
||||||
Data map[string]any
|
|
||||||
FlashData map[string]any // Flash data for next request
|
|
||||||
OldFlash map[string]any // Flash data from previous request (to be cleared)
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
LastUsed time.Time
|
|
||||||
Expiry time.Time
|
|
||||||
dirty bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
sessionPool = sync.Pool{
|
|
||||||
New: func() any {
|
|
||||||
return &Session{
|
|
||||||
Data: make(map[string]any, 8),
|
|
||||||
FlashData: make(map[string]any, 4),
|
|
||||||
OldFlash: make(map[string]any, 4),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
bufPool = benc.NewBufPool(benc.WithBufferSize(4096))
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewSession creates a new session with the given ID
|
|
||||||
func NewSession(id string, maxAge int) *Session {
|
|
||||||
s := sessionPool.Get().(*Session)
|
|
||||||
now := time.Now()
|
|
||||||
*s = Session{
|
|
||||||
ID: id,
|
|
||||||
Data: s.Data, // Reuse maps
|
|
||||||
FlashData: s.FlashData,
|
|
||||||
OldFlash: s.OldFlash,
|
|
||||||
CreatedAt: now,
|
|
||||||
UpdatedAt: now,
|
|
||||||
LastUsed: now,
|
|
||||||
Expiry: now.Add(time.Duration(maxAge) * time.Second),
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release returns the session to the pool
|
|
||||||
func (s *Session) Release() {
|
|
||||||
for k := range s.Data {
|
|
||||||
delete(s.Data, k)
|
|
||||||
}
|
|
||||||
for k := range s.FlashData {
|
|
||||||
delete(s.FlashData, k)
|
|
||||||
}
|
|
||||||
for k := range s.OldFlash {
|
|
||||||
delete(s.OldFlash, k)
|
|
||||||
}
|
|
||||||
sessionPool.Put(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns a deep copy of a value
|
|
||||||
func (s *Session) Get(key string) any {
|
|
||||||
if v, ok := s.Data[key]; ok {
|
|
||||||
return deepCopy(v)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTable returns a value as a table
|
|
||||||
func (s *Session) GetTable(key string) map[string]any {
|
|
||||||
if v := s.Get(key); v != nil {
|
|
||||||
if t, ok := v.(map[string]any); ok {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAll returns a deep copy of all session data including flash data
|
|
||||||
func (s *Session) GetAll() map[string]any {
|
|
||||||
copy := make(map[string]any, len(s.Data)+len(s.FlashData)+len(s.OldFlash))
|
|
||||||
for k, v := range s.Data {
|
|
||||||
copy[k] = deepCopy(v)
|
|
||||||
}
|
|
||||||
// Include current flash data
|
|
||||||
for k, v := range s.FlashData {
|
|
||||||
copy[k] = deepCopy(v)
|
|
||||||
}
|
|
||||||
// Include old flash data (still available this request)
|
|
||||||
for k, v := range s.OldFlash {
|
|
||||||
if _, exists := copy[k]; !exists { // Don't override new flash
|
|
||||||
copy[k] = deepCopy(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return copy
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set stores a value in the session
|
|
||||||
func (s *Session) Set(key string, value any) {
|
|
||||||
if existing, ok := s.Data[key]; ok && deepEqual(existing, value) {
|
|
||||||
return // No change
|
|
||||||
}
|
|
||||||
s.Data[key] = value
|
|
||||||
s.UpdatedAt = time.Now()
|
|
||||||
s.dirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSafe stores a value with validation
|
|
||||||
func (s *Session) SetSafe(key string, value any) error {
|
|
||||||
if err := validate(value); err != nil {
|
|
||||||
return fmt.Errorf("session.SetSafe: %w", err)
|
|
||||||
}
|
|
||||||
s.Set(key, value)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetTable is a convenience method for setting table data
|
|
||||||
func (s *Session) SetTable(key string, table map[string]any) error {
|
|
||||||
return s.SetSafe(key, table)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flash stores a value that will be available for the next request only
|
|
||||||
func (s *Session) Flash(key string, value any) {
|
|
||||||
s.FlashData[key] = value
|
|
||||||
s.UpdatedAt = time.Now()
|
|
||||||
s.dirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// FlashSafe stores a flash value with validation
|
|
||||||
func (s *Session) FlashSafe(key string, value any) error {
|
|
||||||
if err := validate(value); err != nil {
|
|
||||||
return fmt.Errorf("session.FlashSafe: %w", err)
|
|
||||||
}
|
|
||||||
s.Flash(key, value)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFlash returns a flash value (from either current or old flash)
|
|
||||||
func (s *Session) GetFlash(key string) any {
|
|
||||||
// Check current flash first
|
|
||||||
if v, ok := s.FlashData[key]; ok {
|
|
||||||
return deepCopy(v)
|
|
||||||
}
|
|
||||||
// Check old flash
|
|
||||||
if v, ok := s.OldFlash[key]; ok {
|
|
||||||
return deepCopy(v)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasFlash checks if a flash key exists
|
|
||||||
func (s *Session) HasFlash(key string) bool {
|
|
||||||
_, inNew := s.FlashData[key]
|
|
||||||
_, inOld := s.OldFlash[key]
|
|
||||||
return inNew || inOld
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllFlash returns all flash data (both current and old)
|
|
||||||
func (s *Session) GetAllFlash() map[string]any {
|
|
||||||
flash := make(map[string]any, len(s.FlashData)+len(s.OldFlash))
|
|
||||||
// Add old flash first
|
|
||||||
for k, v := range s.OldFlash {
|
|
||||||
flash[k] = deepCopy(v)
|
|
||||||
}
|
|
||||||
// Add current flash (overwrites old if same key)
|
|
||||||
for k, v := range s.FlashData {
|
|
||||||
flash[k] = deepCopy(v)
|
|
||||||
}
|
|
||||||
return flash
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdvanceFlash moves current flash to old flash and clears old flash
|
|
||||||
// This should be called at the start of each request
|
|
||||||
func (s *Session) AdvanceFlash() {
|
|
||||||
// Clear old flash
|
|
||||||
for k := range s.OldFlash {
|
|
||||||
delete(s.OldFlash, k)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move current flash to old flash
|
|
||||||
if len(s.FlashData) > 0 {
|
|
||||||
for k, v := range s.FlashData {
|
|
||||||
s.OldFlash[k] = v
|
|
||||||
}
|
|
||||||
// Clear current flash
|
|
||||||
for k := range s.FlashData {
|
|
||||||
delete(s.FlashData, k)
|
|
||||||
}
|
|
||||||
s.dirty = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes a value from the session
|
|
||||||
func (s *Session) Delete(key string) {
|
|
||||||
delete(s.Data, key)
|
|
||||||
s.UpdatedAt = time.Now()
|
|
||||||
s.dirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear removes all data from the session
|
|
||||||
func (s *Session) Clear() {
|
|
||||||
s.Data = make(map[string]any, 8)
|
|
||||||
s.UpdatedAt = time.Now()
|
|
||||||
s.dirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearFlash removes all flash data
|
|
||||||
func (s *Session) ClearFlash() {
|
|
||||||
if len(s.FlashData) > 0 || len(s.OldFlash) > 0 {
|
|
||||||
s.FlashData = make(map[string]any, 4)
|
|
||||||
s.OldFlash = make(map[string]any, 4)
|
|
||||||
s.dirty = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsExpired checks if the session has expired
|
|
||||||
func (s *Session) IsExpired() bool {
|
|
||||||
return time.Now().After(s.Expiry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateLastUsed updates the last used time
|
|
||||||
func (s *Session) UpdateLastUsed() {
|
|
||||||
now := time.Now()
|
|
||||||
if now.Sub(s.LastUsed) > 5*time.Second {
|
|
||||||
s.LastUsed = now
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDirty returns if the session has unsaved changes
|
|
||||||
func (s *Session) IsDirty() bool {
|
|
||||||
return s.dirty
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetDirty marks the session as clean after saving
|
|
||||||
func (s *Session) ResetDirty() {
|
|
||||||
s.dirty = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// SizePlain calculates the size needed to marshal the session
|
|
||||||
func (s *Session) SizePlain() int {
|
|
||||||
return bstd.SizeString(s.ID) +
|
|
||||||
bstd.SizeMap(s.Data, bstd.SizeString, sizeAny) +
|
|
||||||
bstd.SizeMap(s.FlashData, bstd.SizeString, sizeAny) +
|
|
||||||
bstd.SizeMap(s.OldFlash, bstd.SizeString, sizeAny) +
|
|
||||||
bstd.SizeInt64()*4
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalPlain serializes the session to binary
|
|
||||||
func (s *Session) MarshalPlain(n int, b []byte) int {
|
|
||||||
n = bstd.MarshalString(n, b, s.ID)
|
|
||||||
n = bstd.MarshalMap(n, b, s.Data, bstd.MarshalString, marshalAny)
|
|
||||||
n = bstd.MarshalMap(n, b, s.FlashData, bstd.MarshalString, marshalAny)
|
|
||||||
n = bstd.MarshalMap(n, b, s.OldFlash, bstd.MarshalString, marshalAny)
|
|
||||||
n = bstd.MarshalInt64(n, b, s.CreatedAt.Unix())
|
|
||||||
n = bstd.MarshalInt64(n, b, s.UpdatedAt.Unix())
|
|
||||||
n = bstd.MarshalInt64(n, b, s.LastUsed.Unix())
|
|
||||||
return bstd.MarshalInt64(n, b, s.Expiry.Unix())
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalPlain deserializes the session from binary
|
|
||||||
func (s *Session) UnmarshalPlain(n int, b []byte) (int, error) {
|
|
||||||
var err error
|
|
||||||
n, s.ID, err = bstd.UnmarshalString(n, b)
|
|
||||||
if err != nil {
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
n, s.Data, err = bstd.UnmarshalMap[string, any](n, b, bstd.UnmarshalString, unmarshalAny)
|
|
||||||
if err != nil {
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
n, s.FlashData, err = bstd.UnmarshalMap[string, any](n, b, bstd.UnmarshalString, unmarshalAny)
|
|
||||||
if err != nil {
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
n, s.OldFlash, err = bstd.UnmarshalMap[string, any](n, b, bstd.UnmarshalString, unmarshalAny)
|
|
||||||
if err != nil {
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var ts int64
|
|
||||||
for _, t := range []*time.Time{&s.CreatedAt, &s.UpdatedAt, &s.LastUsed, &s.Expiry} {
|
|
||||||
n, ts, err = bstd.UnmarshalInt64(n, b)
|
|
||||||
if err != nil {
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
*t = time.Unix(ts, 0)
|
|
||||||
}
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal serializes the session using benc
|
|
||||||
func (s *Session) Marshal() ([]byte, error) {
|
|
||||||
return bufPool.Marshal(s.SizePlain(), func(b []byte) int {
|
|
||||||
return s.MarshalPlain(0, b)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal deserializes a session using benc
|
|
||||||
func Unmarshal(data []byte) (*Session, error) {
|
|
||||||
s := sessionPool.Get().(*Session)
|
|
||||||
if _, err := s.UnmarshalPlain(0, data); err != nil {
|
|
||||||
s.Release()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type identifiers
|
|
||||||
const (
|
|
||||||
typeNull byte = 0
|
|
||||||
typeString byte = 1
|
|
||||||
typeInt byte = 2
|
|
||||||
typeFloat byte = 3
|
|
||||||
typeBool byte = 4
|
|
||||||
typeBytes byte = 5
|
|
||||||
typeTable byte = 6
|
|
||||||
typeArray byte = 7
|
|
||||||
)
|
|
||||||
|
|
||||||
// sizeAny calculates the size needed for any value
|
|
||||||
func sizeAny(v any) int {
|
|
||||||
if v == nil {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
size := 1 // type byte
|
|
||||||
switch v := v.(type) {
|
|
||||||
case string:
|
|
||||||
size += bstd.SizeString(v)
|
|
||||||
case int:
|
|
||||||
size += bstd.SizeInt64()
|
|
||||||
case int64:
|
|
||||||
size += bstd.SizeInt64()
|
|
||||||
case float64:
|
|
||||||
size += bstd.SizeFloat64()
|
|
||||||
case bool:
|
|
||||||
size += bstd.SizeBool()
|
|
||||||
case []byte:
|
|
||||||
size += bstd.SizeBytes(v)
|
|
||||||
case map[string]any:
|
|
||||||
size += bstd.SizeMap(v, bstd.SizeString, sizeAny)
|
|
||||||
case []any:
|
|
||||||
size += bstd.SizeSlice(v, sizeAny)
|
|
||||||
default:
|
|
||||||
size += bstd.SizeString("unknown")
|
|
||||||
}
|
|
||||||
return size
|
|
||||||
}
|
|
||||||
|
|
||||||
// marshalAny serializes any value
|
|
||||||
func marshalAny(n int, b []byte, v any) int {
|
|
||||||
if v == nil {
|
|
||||||
b[n] = typeNull
|
|
||||||
return n + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := v.(type) {
|
|
||||||
case string:
|
|
||||||
b[n] = typeString
|
|
||||||
return bstd.MarshalString(n+1, b, v)
|
|
||||||
case int:
|
|
||||||
b[n] = typeInt
|
|
||||||
return bstd.MarshalInt64(n+1, b, int64(v))
|
|
||||||
case int64:
|
|
||||||
b[n] = typeInt
|
|
||||||
return bstd.MarshalInt64(n+1, b, v)
|
|
||||||
case float64:
|
|
||||||
b[n] = typeFloat
|
|
||||||
return bstd.MarshalFloat64(n+1, b, v)
|
|
||||||
case bool:
|
|
||||||
b[n] = typeBool
|
|
||||||
return bstd.MarshalBool(n+1, b, v)
|
|
||||||
case []byte:
|
|
||||||
b[n] = typeBytes
|
|
||||||
return bstd.MarshalBytes(n+1, b, v)
|
|
||||||
case map[string]any:
|
|
||||||
b[n] = typeTable
|
|
||||||
return bstd.MarshalMap(n+1, b, v, bstd.MarshalString, marshalAny)
|
|
||||||
case []any:
|
|
||||||
b[n] = typeArray
|
|
||||||
return bstd.MarshalSlice(n+1, b, v, marshalAny)
|
|
||||||
default:
|
|
||||||
b[n] = typeString
|
|
||||||
return bstd.MarshalString(n+1, b, "unknown")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// unmarshalAny deserializes any value
|
|
||||||
func unmarshalAny(n int, b []byte) (int, any, error) {
|
|
||||||
if len(b) <= n {
|
|
||||||
return n, nil, benc.ErrBufTooSmall
|
|
||||||
}
|
|
||||||
|
|
||||||
switch b[n] {
|
|
||||||
case typeNull:
|
|
||||||
return n + 1, nil, nil
|
|
||||||
case typeString:
|
|
||||||
return bstd.UnmarshalString(n+1, b)
|
|
||||||
case typeInt:
|
|
||||||
n, v, err := bstd.UnmarshalInt64(n+1, b)
|
|
||||||
return n, v, err
|
|
||||||
case typeFloat:
|
|
||||||
return bstd.UnmarshalFloat64(n+1, b)
|
|
||||||
case typeBool:
|
|
||||||
return bstd.UnmarshalBool(n+1, b)
|
|
||||||
case typeBytes:
|
|
||||||
return bstd.UnmarshalBytesCopied(n+1, b)
|
|
||||||
case typeTable:
|
|
||||||
return bstd.UnmarshalMap[string, any](n+1, b, bstd.UnmarshalString, unmarshalAny)
|
|
||||||
case typeArray:
|
|
||||||
return bstd.UnmarshalSlice[any](n+1, b, unmarshalAny)
|
|
||||||
default:
|
|
||||||
return n + 1, nil, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// deepCopy creates a deep copy of any value
|
|
||||||
func deepCopy(v any) any {
|
|
||||||
switch v := v.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
cp := make(map[string]any, len(v))
|
|
||||||
for k, val := range v {
|
|
||||||
cp[k] = deepCopy(val)
|
|
||||||
}
|
|
||||||
return cp
|
|
||||||
case []any:
|
|
||||||
cp := make([]any, len(v))
|
|
||||||
for i, val := range v {
|
|
||||||
cp[i] = deepCopy(val)
|
|
||||||
}
|
|
||||||
return cp
|
|
||||||
default:
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate ensures a value can be safely serialized
|
|
||||||
func validate(v any) error {
|
|
||||||
switch v := v.(type) {
|
|
||||||
case nil, string, int, int64, float64, bool, []byte:
|
|
||||||
return nil
|
|
||||||
case map[string]any:
|
|
||||||
for k, val := range v {
|
|
||||||
if err := validate(val); err != nil {
|
|
||||||
return fmt.Errorf("invalid value for key %q: %w", k, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case []any:
|
|
||||||
for i, val := range v {
|
|
||||||
if err := validate(val); err != nil {
|
|
||||||
return fmt.Errorf("invalid value at index %d: %w", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported type: %T", v)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// deepEqual efficiently compares two values for deep equality
|
|
||||||
func deepEqual(a, b any) bool {
|
|
||||||
if a == b {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if a == nil || b == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch va := a.(type) {
|
|
||||||
case string:
|
|
||||||
if vb, ok := b.(string); ok {
|
|
||||||
return va == vb
|
|
||||||
}
|
|
||||||
case int:
|
|
||||||
if vb, ok := b.(int); ok {
|
|
||||||
return va == vb
|
|
||||||
}
|
|
||||||
if vb, ok := b.(int64); ok {
|
|
||||||
return int64(va) == vb
|
|
||||||
}
|
|
||||||
case int64:
|
|
||||||
if vb, ok := b.(int64); ok {
|
|
||||||
return va == vb
|
|
||||||
}
|
|
||||||
if vb, ok := b.(int); ok {
|
|
||||||
return va == int64(vb)
|
|
||||||
}
|
|
||||||
case float64:
|
|
||||||
if vb, ok := b.(float64); ok {
|
|
||||||
return va == vb
|
|
||||||
}
|
|
||||||
case bool:
|
|
||||||
if vb, ok := b.(bool); ok {
|
|
||||||
return va == vb
|
|
||||||
}
|
|
||||||
case []byte:
|
|
||||||
if vb, ok := b.([]byte); ok {
|
|
||||||
if len(va) != len(vb) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i, v := range va {
|
|
||||||
if v != vb[i] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
case map[string]any:
|
|
||||||
if vb, ok := b.(map[string]any); ok {
|
|
||||||
if len(va) != len(vb) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for k, v := range va {
|
|
||||||
if bv, exists := vb[k]; !exists || !deepEqual(v, bv) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
case []any:
|
|
||||||
if vb, ok := b.([]any); ok {
|
|
||||||
if len(va) != len(vb) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i, v := range va {
|
|
||||||
if !deepEqual(v, vb[i]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsEmpty returns true if the session has no data
|
|
||||||
func (s *Session) IsEmpty() bool {
|
|
||||||
return len(s.Data) == 0 && len(s.FlashData) == 0 && len(s.OldFlash) == 0
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
package metadata
|
|
||||||
|
|
||||||
const Version = "1.0"
|
|
||||||
|
|
||||||
var (
|
|
||||||
BuildTime = "unknown"
|
|
||||||
GitCommit = "unknown"
|
|
||||||
GoVersion = "unknown"
|
|
||||||
)
|
|
64
moonshark.go
64
moonshark.go
@ -1,68 +1,4 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"Moonshark/http"
|
|
||||||
|
|
||||||
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
|
||||||
fmt.Fprintf(os.Stderr, "Usage: %s <lua_file>\n", os.Args[0])
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
luaFile := os.Args[1]
|
|
||||||
|
|
||||||
if _, err := os.Stat(luaFile); os.IsNotExist(err) {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: File '%s' not found\n", luaFile)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create long-lived LuaJIT state
|
|
||||||
L := luajit.New(true)
|
|
||||||
if L == nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: Failed to create Lua state\n")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
L.Cleanup()
|
|
||||||
L.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Register HTTP functions
|
|
||||||
if err := http.RegisterHTTPFunctions(L); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error registering HTTP functions: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the Lua file
|
|
||||||
if err := L.DoFile(luaFile); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle return value for immediate exit
|
|
||||||
if L.GetTop() > 0 {
|
|
||||||
if L.IsNumber(1) {
|
|
||||||
exitCode := int(L.ToNumber(1))
|
|
||||||
if exitCode != 0 {
|
|
||||||
os.Exit(exitCode)
|
|
||||||
}
|
|
||||||
} else if L.IsBoolean(1) && !L.ToBoolean(1) {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep running for HTTP server
|
|
||||||
fmt.Println("Script executed. Press Ctrl+C to exit.")
|
|
||||||
c := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
|
||||||
<-c
|
|
||||||
fmt.Println("\nShutting down...")
|
|
||||||
}
|
}
|
||||||
|
189
test.lua
189
test.lua
@ -1,189 +0,0 @@
|
|||||||
-- Example HTTP server with string-based routing, parameters, and wildcards
|
|
||||||
|
|
||||||
print("Starting Moonshark HTTP server with string routing...")
|
|
||||||
|
|
||||||
-- Start HTTP server
|
|
||||||
http.listen(3000)
|
|
||||||
|
|
||||||
-- Home page
|
|
||||||
http.route("GET", "/", function(req)
|
|
||||||
local visits = session.get("visits") or 0
|
|
||||||
visits = visits + 1
|
|
||||||
session.set("visits", visits)
|
|
||||||
|
|
||||||
return http.html([[
|
|
||||||
<h1>Welcome to Moonshark!</h1>
|
|
||||||
<p>You've visited this page ]] .. visits .. [[ times.</p>
|
|
||||||
<p><a href="/login">Login</a> | <a href="/users/123">User Profile</a></p>
|
|
||||||
<p><a href="/api/test">API Test</a> | <a href="/files/docs/readme.txt">File Access</a></p>
|
|
||||||
]])
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- User profile with dynamic parameter
|
|
||||||
http.route("GET", "/users/:id", function(req)
|
|
||||||
local userId = req.params.id
|
|
||||||
return http.html([[
|
|
||||||
<h2>User Profile</h2>
|
|
||||||
<p>User ID: ]] .. userId .. [[</p>
|
|
||||||
<p><a href="/users/]] .. userId .. [[/posts">View Posts</a></p>
|
|
||||||
<p><a href="/">Home</a></p>
|
|
||||||
]])
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- User posts with multiple parameters
|
|
||||||
http.route("GET", "/users/:id/posts", function(req)
|
|
||||||
local userId = req.params.id
|
|
||||||
return http.json({
|
|
||||||
user_id = userId,
|
|
||||||
posts = {
|
|
||||||
{id = 1, title = "First Post", content = "Hello world!"},
|
|
||||||
{id = 2, title = "Second Post", content = "Learning Lua routing!"}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Blog post with slug parameter
|
|
||||||
http.route("GET", "/blog/:slug", function(req)
|
|
||||||
local slug = req.params.slug
|
|
||||||
return http.html([[
|
|
||||||
<h1>Blog Post: ]] .. slug .. [[</h1>
|
|
||||||
<p>This is the content for blog post "]] .. slug .. [["</p>
|
|
||||||
<p><a href="/blog/]] .. slug .. [[/comments">View Comments</a></p>
|
|
||||||
<p><a href="/">Home</a></p>
|
|
||||||
]])
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Blog comments
|
|
||||||
http.route("GET", "/blog/:slug/comments", function(req)
|
|
||||||
local slug = req.params.slug
|
|
||||||
return http.json({
|
|
||||||
blog_slug = slug,
|
|
||||||
comments = {
|
|
||||||
{author = "Alice", comment = "Great post!"},
|
|
||||||
{author = "Bob", comment = "Very informative."}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Wildcard route for file serving
|
|
||||||
http.route("GET", "/files/*path", function(req)
|
|
||||||
local filePath = req.params.path
|
|
||||||
return http.html([[
|
|
||||||
<h2>File Access</h2>
|
|
||||||
<p>Requested file: ]] .. filePath .. [[</p>
|
|
||||||
<p>In a real application, this would serve the file content.</p>
|
|
||||||
<p><a href="/">Home</a></p>
|
|
||||||
]])
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- API endpoints with parameters
|
|
||||||
http.route("GET", "/api/users/:id", function(req)
|
|
||||||
local userId = req.params.id
|
|
||||||
return http.json({
|
|
||||||
id = tonumber(userId),
|
|
||||||
name = "User " .. userId,
|
|
||||||
email = "user" .. userId .. "@example.com",
|
|
||||||
active = true
|
|
||||||
})
|
|
||||||
end)
|
|
||||||
|
|
||||||
http.route("PUT", "/api/users/:id", function(req)
|
|
||||||
local userId = req.params.id
|
|
||||||
local userData = req.form
|
|
||||||
|
|
||||||
return http.json({
|
|
||||||
success = true,
|
|
||||||
message = "User " .. userId .. " updated",
|
|
||||||
data = userData
|
|
||||||
})
|
|
||||||
end)
|
|
||||||
|
|
||||||
http.route("DELETE", "/api/users/:id", function(req)
|
|
||||||
local userId = req.params.id
|
|
||||||
return http.json({
|
|
||||||
success = true,
|
|
||||||
message = "User " .. userId .. " deleted"
|
|
||||||
})
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Login form with CSRF protection
|
|
||||||
http.route("GET", "/login", function(req)
|
|
||||||
return http.html([[
|
|
||||||
<h2>Login</h2>
|
|
||||||
<form method="POST" action="/login">
|
|
||||||
]] .. csrf.field() .. [[
|
|
||||||
<input type="text" name="username" placeholder="Username" required><br>
|
|
||||||
<input type="password" name="password" placeholder="Password" required><br>
|
|
||||||
<button type="submit">Login</button>
|
|
||||||
</form>
|
|
||||||
<p><a href="/">Home</a></p>
|
|
||||||
]])
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Handle login POST
|
|
||||||
http.route("POST", "/login", function(req)
|
|
||||||
if not csrf.validate() then
|
|
||||||
http.status(403)
|
|
||||||
return "CSRF token invalid"
|
|
||||||
end
|
|
||||||
|
|
||||||
local username = req.form.username
|
|
||||||
local password = req.form.password
|
|
||||||
|
|
||||||
if username == "admin" and password == "secret" then
|
|
||||||
session.set("user", username)
|
|
||||||
session.flash("success", "Login successful!")
|
|
||||||
return http.redirect("/dashboard")
|
|
||||||
else
|
|
||||||
session.flash("error", "Invalid credentials")
|
|
||||||
return http.redirect("/login")
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Dashboard (requires login)
|
|
||||||
http.route("GET", "/dashboard", function(req)
|
|
||||||
local user = session.get("user")
|
|
||||||
if not user then
|
|
||||||
return http.redirect("/login")
|
|
||||||
end
|
|
||||||
|
|
||||||
local success = session.get_flash("success")
|
|
||||||
local error = session.get_flash("error")
|
|
||||||
|
|
||||||
return http.html([[
|
|
||||||
<h2>Dashboard</h2>
|
|
||||||
]] .. (success and ("<p style='color:green'>" .. success .. "</p>") or "") .. [[
|
|
||||||
]] .. (error and ("<p style='color:red'>" .. error .. "</p>") or "") .. [[
|
|
||||||
<p>Welcome, ]] .. user .. [[!</p>
|
|
||||||
<p><a href="/logout">Logout</a></p>
|
|
||||||
<p><a href="/">Home</a></p>
|
|
||||||
]])
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Logout
|
|
||||||
http.route("GET", "/logout", function(req)
|
|
||||||
session.set("user", nil)
|
|
||||||
session.flash("info", "You have been logged out")
|
|
||||||
return http.redirect("/")
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Catch-all route for 404s (must be last)
|
|
||||||
http.route("GET", "*path", function(req)
|
|
||||||
http.status(404)
|
|
||||||
return http.html([[
|
|
||||||
<h1>404 - Page Not Found</h1>
|
|
||||||
<p>The requested path "]] .. req.params.path .. [[" was not found.</p>
|
|
||||||
<p><a href="/">Go Home</a></p>
|
|
||||||
]])
|
|
||||||
end)
|
|
||||||
|
|
||||||
print("Server configured with string routing. Listening on http://localhost:3000")
|
|
||||||
print("Try these routes:")
|
|
||||||
print(" GET /")
|
|
||||||
print(" GET /users/123")
|
|
||||||
print(" GET /users/456/posts")
|
|
||||||
print(" GET /blog/my-first-post")
|
|
||||||
print(" GET /blog/lua-tutorial/comments")
|
|
||||||
print(" GET /files/docs/readme.txt")
|
|
||||||
print(" GET /api/users/789")
|
|
||||||
print(" GET /nonexistent (404 handler)")
|
|
Loading…
x
Reference in New Issue
Block a user