diff --git a/.gitignore b/.gitignore index fe0b89c..bf20b1c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,4 @@ luajit/.git go.work # Test directories and files -/config.lua -test/ -/init.lua -/moonshark +test.lua diff --git a/go.mod b/go.mod index 8cce1dd..a1219f3 100644 --- a/go.mod +++ b/go.mod @@ -1,22 +1,3 @@ module Moonshark 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 -) diff --git a/go.sum b/go.sum index 2acd72b..e69de29 100644 --- a/go.sum +++ b/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= diff --git a/http/http.go b/http/http.go deleted file mode 100644 index 060fc5b..0000000 --- a/http/http.go +++ /dev/null @@ -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) -} diff --git a/http/http.lua b/http/http.lua deleted file mode 100644 index fef7630..0000000 --- a/http/http.lua +++ /dev/null @@ -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 '' -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 \ No newline at end of file diff --git a/http/router/router.go b/http/router/router.go deleted file mode 100644 index 817b7c5..0000000 --- a/http/router/router.go +++ /dev/null @@ -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 -} diff --git a/http/sessions/manager.go b/http/sessions/manager.go deleted file mode 100644 index 5d8dd2d..0000000 --- a/http/sessions/manager.go +++ /dev/null @@ -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, - } -} diff --git a/http/sessions/session.go b/http/sessions/session.go deleted file mode 100644 index daabb88..0000000 --- a/http/sessions/session.go +++ /dev/null @@ -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 -} diff --git a/metadata/metadata.go b/metadata/metadata.go deleted file mode 100644 index 66d58bd..0000000 --- a/metadata/metadata.go +++ /dev/null @@ -1,9 +0,0 @@ -package metadata - -const Version = "1.0" - -var ( - BuildTime = "unknown" - GitCommit = "unknown" - GoVersion = "unknown" -) diff --git a/moonshark.go b/moonshark.go index f34d399..da29a2c 100644 --- a/moonshark.go +++ b/moonshark.go @@ -1,68 +1,4 @@ package main -import ( - "fmt" - "os" - "os/signal" - "syscall" - - "Moonshark/http" - - luajit "git.sharkk.net/Sky/LuaJIT-to-Go" -) - func main() { - if len(os.Args) < 2 { - fmt.Fprintf(os.Stderr, "Usage: %s \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...") } diff --git a/test.lua b/test.lua deleted file mode 100644 index 4fea308..0000000 --- a/test.lua +++ /dev/null @@ -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([[ -

Welcome to Moonshark!

-

You've visited this page ]] .. visits .. [[ times.

-

Login | User Profile

-

API Test | File Access

- ]]) -end) - --- User profile with dynamic parameter -http.route("GET", "/users/:id", function(req) - local userId = req.params.id - return http.html([[ -

User Profile

-

User ID: ]] .. userId .. [[

-

View Posts

-

Home

- ]]) -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([[ -

Blog Post: ]] .. slug .. [[

-

This is the content for blog post "]] .. slug .. [["

-

View Comments

-

Home

- ]]) -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([[ -

File Access

-

Requested file: ]] .. filePath .. [[

-

In a real application, this would serve the file content.

-

Home

- ]]) -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([[ -

Login

-
- ]] .. csrf.field() .. [[ -
-
- -
-

Home

- ]]) -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([[ -

Dashboard

- ]] .. (success and ("

" .. success .. "

") or "") .. [[ - ]] .. (error and ("

" .. error .. "

") or "") .. [[ -

Welcome, ]] .. user .. [[!

-

Logout

-

Home

- ]]) -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([[ -

404 - Page Not Found

-

The requested path "]] .. req.params.path .. [[" was not found.

-

Go Home

- ]]) -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)") \ No newline at end of file