This commit is contained in:
Sky Johnson 2025-07-14 19:11:43 -05:00
parent 843e318e01
commit f75ba90f74
11 changed files with 1 additions and 2073 deletions

5
.gitignore vendored
View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -1,9 +0,0 @@
package metadata
const Version = "1.0"
var (
BuildTime = "unknown"
GitCommit = "unknown"
GoVersion = "unknown"
)

View File

@ -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
View File

@ -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)")