From e8ad16ccdcac8a12667e4325e8ca57e021935f36 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Tue, 15 Jul 2025 20:24:12 -0500 Subject: [PATCH] first pass on HTTP module --- functions/http.go | 495 ++++++++++++++++++++++++++++++++++++++++++ functions/registry.go | 1 + go.mod | 7 + go.sum | 10 + modules/http.lua | 277 +++++++++++++++++++++++ moonshark.go | 36 +++ 6 files changed, 826 insertions(+) create mode 100644 functions/http.go create mode 100644 modules/http.lua diff --git a/functions/http.go b/functions/http.go new file mode 100644 index 0000000..6106cb4 --- /dev/null +++ b/functions/http.go @@ -0,0 +1,495 @@ +package functions + +import ( + "fmt" + "sync" + "time" + + luajit "git.sharkk.net/Sky/LuaJIT-to-Go" + "github.com/valyala/fasthttp" +) + +// Handler is a fasthttp request handler with parameters +type Handler func(ctx *fasthttp.RequestCtx, params []string) + +type node struct { + segment string + handler Handler + children []*node + isDynamic bool + isWildcard bool + maxParams uint8 +} + +type Router struct { + get, post, put, patch, delete *node + paramsBuffer []string +} + +func newRouter() *Router { + return &Router{ + get: &node{}, + post: &node{}, + put: &node{}, + patch: &node{}, + delete: &node{}, + paramsBuffer: make([]string, 64), + } +} + +// HTTPServer with efficient serialized Lua handling +type HTTPServer struct { + server *fasthttp.Server + router *Router + addr string + running bool + mu sync.RWMutex + luaMu sync.Mutex // Serializes Lua calls +} + +var ( + serverRegistry = struct { + sync.RWMutex + servers map[int]*HTTPServer + nextID int + }{ + servers: make(map[int]*HTTPServer), + nextID: 1, + } +) + +func GetHTTPFunctions() map[string]luajit.GoFunction { + return map[string]luajit.GoFunction{ + "http_create_server": func(s *luajit.State) int { + server := &HTTPServer{ + server: &fasthttp.Server{ + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 60 * time.Second, + }, + router: newRouter(), + } + + server.server.Handler = server.handleRequest + + serverRegistry.Lock() + id := serverRegistry.nextID + serverRegistry.nextID++ + serverRegistry.servers[id] = server + serverRegistry.Unlock() + + s.PushNumber(float64(id)) + return 1 + }, + + "http_server_listen": func(s *luajit.State) int { + if err := s.CheckExactArgs(2); err != nil { + return s.PushError("http_server_listen: %v", err) + } + + serverID, err := s.SafeToNumber(1) + if err != nil || serverID != float64(int(serverID)) { + return s.PushError("http_server_listen: server ID must be an integer") + } + + addr, err := s.SafeToString(2) + if err != nil { + return s.PushError("http_server_listen: address must be a string") + } + + serverRegistry.RLock() + server, exists := serverRegistry.servers[int(serverID)] + serverRegistry.RUnlock() + + if !exists { + return s.PushError("http_server_listen: server not found") + } + + server.mu.Lock() + if server.running { + server.mu.Unlock() + return s.PushError("http_server_listen: server already running") + } + + server.addr = addr + server.running = true + server.mu.Unlock() + + go func() { + err := server.server.ListenAndServe(addr) + if err != nil { + server.mu.Lock() + server.running = false + server.mu.Unlock() + } + }() + + s.PushBoolean(true) + return 1 + }, + + "http_server_stop": func(s *luajit.State) int { + if err := s.CheckMinArgs(1); err != nil { + return s.PushError("http_server_stop: %v", err) + } + + serverID, err := s.SafeToNumber(1) + if err != nil || serverID != float64(int(serverID)) { + return s.PushError("http_server_stop: server ID must be an integer") + } + + serverRegistry.RLock() + server, exists := serverRegistry.servers[int(serverID)] + serverRegistry.RUnlock() + + if !exists { + return s.PushError("http_server_stop: server not found") + } + + server.mu.Lock() + if !server.running { + server.mu.Unlock() + s.PushBoolean(false) + return 1 + } + + server.running = false + server.mu.Unlock() + + if err := server.server.Shutdown(); err != nil { + return s.PushError("http_server_stop: %v", err) + } + + s.PushBoolean(true) + return 1 + }, + + "http_server_get": createRouteHandler("GET"), + "http_server_post": createRouteHandler("POST"), + "http_server_put": createRouteHandler("PUT"), + "http_server_patch": createRouteHandler("PATCH"), + "http_server_delete": createRouteHandler("DELETE"), + + "http_server_is_running": func(s *luajit.State) int { + if err := s.CheckMinArgs(1); err != nil { + return s.PushError("http_server_is_running: %v", err) + } + + serverID, err := s.SafeToNumber(1) + if err != nil || serverID != float64(int(serverID)) { + return s.PushError("http_server_is_running: server ID must be an integer") + } + + serverRegistry.RLock() + server, exists := serverRegistry.servers[int(serverID)] + serverRegistry.RUnlock() + + if !exists { + s.PushBoolean(false) + return 1 + } + + server.mu.RLock() + running := server.running + server.mu.RUnlock() + + s.PushBoolean(running) + return 1 + }, + + "http_cleanup_servers": func(s *luajit.State) int { + serverRegistry.Lock() + for id, server := range serverRegistry.servers { + server.mu.Lock() + if server.running { + server.server.Shutdown() + server.running = false + } + server.mu.Unlock() + delete(serverRegistry.servers, id) + } + serverRegistry.Unlock() + + s.PushBoolean(true) + return 1 + }, + } +} + +func createRouteHandler(method string) luajit.GoFunction { + return func(s *luajit.State) int { + if err := s.CheckExactArgs(3); err != nil { + return s.PushError("http_server_%s: %v", method, err) + } + + serverID, err := s.SafeToNumber(1) + if err != nil || serverID != float64(int(serverID)) { + return s.PushError("http_server_%s: server ID must be an integer", method) + } + + path, err := s.SafeToString(2) + if err != nil { + return s.PushError("http_server_%s: path must be a string", method) + } + + if !s.IsFunction(3) { + return s.PushError("http_server_%s: handler must be a function", method) + } + + serverRegistry.RLock() + server, exists := serverRegistry.servers[int(serverID)] + serverRegistry.RUnlock() + + if !exists { + return s.PushError("http_server_%s: server not found", method) + } + + luaFunc, err := s.StoreLuaFunction(3) + if err != nil { + return s.PushError("http_server_%s: failed to store function: %v", method, err) + } + + handler := func(ctx *fasthttp.RequestCtx, params []string) { + server.callLuaHandler(ctx, params, luaFunc) + } + + if err := server.router.addRoute(method, path, handler); err != nil { + return s.PushError("http_server_%s: failed to add route: %v", method, err) + } + + s.PushBoolean(true) + return 1 + } +} + +// Router methods +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 + } +} + +func (r *Router) addRoute(method, path string, h Handler) error { + root := r.methodNode(method) + if root == nil { + return fmt.Errorf("unsupported method: %s", method) + } + + if path == "/" { + root.handler = h + return nil + } + + current := root + pos := 0 + lastWC := false + count := uint8(0) + + for { + seg, newPos, more := readSegment(path, pos) + if seg == "" { + break + } + + isDyn := len(seg) > 0 && 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.handler = h + return nil +} + +func (r *Router) lookup(method, path string) (Handler, []string, bool) { + root := r.methodNode(method) + if root == nil { + return nil, nil, false + } + + if path == "/" { + return root.handler, nil, root.handler != nil + } + + buffer := r.paramsBuffer + if cap(buffer) < int(root.maxParams) { + buffer = make([]string, root.maxParams) + r.paramsBuffer = buffer + } + buffer = buffer[:0] + + h, paramCount, found := match(root, path, 0, &buffer) + if !found { + return nil, nil, false + } + + return h, buffer[:paramCount], true +} + +// HTTPServer methods +func (hs *HTTPServer) handleRequest(ctx *fasthttp.RequestCtx) { + method := string(ctx.Method()) + path := string(ctx.Path()) + + handler, params, found := hs.router.lookup(method, path) + if !found { + ctx.SetStatusCode(fasthttp.StatusNotFound) + ctx.WriteString("Not Found") + return + } + + handler(ctx, params) +} + +func (hs *HTTPServer) callLuaHandler(ctx *fasthttp.RequestCtx, params []string, handler *luajit.LuaFunction) { + hs.luaMu.Lock() + defer hs.luaMu.Unlock() + + request := map[string]interface{}{ + "method": string(ctx.Method()), + "path": string(ctx.Path()), + "query": string(ctx.QueryArgs().QueryString()), + "headers": make(map[string]string), + "body": string(ctx.PostBody()), + "remote": ctx.RemoteAddr().String(), + "params": params, + } + + headers := request["headers"].(map[string]string) + ctx.Request.Header.VisitAll(func(key, value []byte) { + headers[string(key)] = string(value) + }) + + response := map[string]interface{}{ + "status": 200, + "headers": make(map[string]string), + "body": "", + } + + results, err := handler.Call(request, response) + if err != nil { + ctx.SetStatusCode(fasthttp.StatusInternalServerError) + ctx.WriteString(fmt.Sprintf("Handler error: %v", err)) + return + } + + if len(results) > 0 { + if respMap, ok := results[0].(map[string]interface{}); ok { + response = respMap + } + } + + if status, ok := response["status"].(int); ok { + ctx.SetStatusCode(status) + } else if status, ok := response["status"].(float64); ok { + ctx.SetStatusCode(int(status)) + } + + if headers, ok := response["headers"].(map[string]interface{}); ok { + for k, v := range headers { + if str, ok := v.(string); ok { + ctx.Response.Header.Set(k, str) + } + } + } + + if body, ok := response["body"].(string); ok { + ctx.WriteString(body) + } +} + +// Utility functions +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) +} + +func match(current *node, path string, start int, params *[]string) (Handler, int, bool) { + paramCount := 0 + + for _, c := range current.children { + if c.isWildcard { + rem := path[start:] + if len(rem) > 0 && rem[0] == '/' { + rem = rem[1:] + } + *params = append(*params, rem) + return c.handler, 1, c.handler != nil + } + } + + seg, pos, more := readSegment(path, start) + if seg == "" { + return current.handler, 0, current.handler != nil + } + + for _, c := range current.children { + if c.segment == seg || c.isDynamic { + if c.isDynamic { + *params = append(*params, seg) + paramCount++ + } + if !more { + return c.handler, paramCount, c.handler != nil + } + h, nestedCount, ok := match(c, path, pos, params) + if ok { + return h, paramCount + nestedCount, true + } + } + } + + return nil, 0, false +} diff --git a/functions/registry.go b/functions/registry.go index a4e564f..0c0535f 100644 --- a/functions/registry.go +++ b/functions/registry.go @@ -18,6 +18,7 @@ func GetAll() Registry { maps.Copy(registry, GetMathFunctions()) maps.Copy(registry, GetFSFunctions()) maps.Copy(registry, GetCryptoFunctions()) + maps.Copy(registry, GetHTTPFunctions()) return registry } diff --git a/go.mod b/go.mod index e2c088f..b11a8df 100644 --- a/go.mod +++ b/go.mod @@ -9,3 +9,10 @@ require github.com/goccy/go-json v0.10.5 require github.com/google/uuid v1.6.0 require golang.org/x/text v0.27.0 + +require ( + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.64.0 +) diff --git a/go.sum b/go.sum index 3ddde42..c5b7adf 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,18 @@ git.sharkk.net/Sky/LuaJIT-to-Go v0.5.6 h1:XytP9R2fWykv0MXIzxggPx5S/PmTkjyZVvUX2sn4EaU= git.sharkk.net/Sky/LuaJIT-to-Go v0.5.6/go.mod h1:HQz+D7AFxOfNbTIogjxP+shEBtz1KKrLlLucU+w07c8= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= 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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/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.64.0 h1:QBygLLQmiAyiXuRhthf0tuRkqAFcrC42dckN2S+N3og= +github.com/valyala/fasthttp v1.64.0/go.mod h1:dGmFxwkWXSK0NbOSJuF7AMVzU+lkHz0wQVvVITv2UQA= +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/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= diff --git a/modules/http.lua b/modules/http.lua new file mode 100644 index 0000000..cd1037d --- /dev/null +++ b/modules/http.lua @@ -0,0 +1,277 @@ +-- modules/http.lua - HTTP server with Go-based routing + +local http = {} + +-- ====================================================================== +-- HTTP SERVER +-- ====================================================================== + +local HTTPServer = {} +HTTPServer.__index = HTTPServer + +function HTTPServer:listen(addr) + local success, err = moonshark.http_server_listen(self.id, addr) + if not success then + error("Failed to start server: " .. (err or "unknown error")) + end + self.addr = addr + return self +end + +function HTTPServer:stop() + local success, err = moonshark.http_server_stop(self.id) + if not success then + error("Failed to stop server: " .. (err or "unknown error")) + end + return self +end + +function HTTPServer:get(path, handler) + if type(handler) ~= "function" then + error("Handler must be a function") + end + + local success, err = moonshark.http_server_get(self.id, path, handler) + if not success then + error("Failed to add GET route: " .. (err or "unknown error")) + end + return self +end + +function HTTPServer:post(path, handler) + if type(handler) ~= "function" then + error("Handler must be a function") + end + + local success, err = moonshark.http_server_post(self.id, path, handler) + if not success then + error("Failed to add POST route: " .. (err or "unknown error")) + end + return self +end + +function HTTPServer:put(path, handler) + if type(handler) ~= "function" then + error("Handler must be a function") + end + + local success, err = moonshark.http_server_put(self.id, path, handler) + if not success then + error("Failed to add PUT route: " .. (err or "unknown error")) + end + return self +end + +function HTTPServer:patch(path, handler) + if type(handler) ~= "function" then + error("Handler must be a function") + end + + local success, err = moonshark.http_server_patch(self.id, path, handler) + if not success then + error("Failed to add PATCH route: " .. (err or "unknown error")) + end + return self +end + +function HTTPServer:delete(path, handler) + if type(handler) ~= "function" then + error("Handler must be a function") + end + + local success, err = moonshark.http_server_delete(self.id, path, handler) + if not success then + error("Failed to add DELETE route: " .. (err or "unknown error")) + end + return self +end + +function HTTPServer:isRunning() + return moonshark.http_server_is_running(self.id) +end + +-- Handle cleanup when server is garbage collected +function HTTPServer:__gc() + if self:isRunning() then + pcall(function() self:stop() end) + end +end + +-- ====================================================================== +-- HTTP MODULE FUNCTIONS +-- ====================================================================== + +function http.createServer(options) + options = options or {} + + local serverID = moonshark.http_create_server() + if not serverID then + error("Failed to create HTTP server") + end + + local server = setmetatable({ + id = serverID, + addr = nil + }, HTTPServer) + + return server +end + +-- Convenience function to create and start server in one call +function http.listen(addr, handler) + local server = http.createServer() + + if handler then + server:get("/*", handler) + end + + return server:listen(addr) +end + +-- Clean up all servers +function http.cleanup() + moonshark.http_cleanup_servers() +end + +-- ====================================================================== +-- REQUEST/RESPONSE HELPERS +-- ====================================================================== + +http.status = { + OK = 200, + CREATED = 201, + NO_CONTENT = 204, + MOVED_PERMANENTLY = 301, + FOUND = 302, + NOT_MODIFIED = 304, + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + FORBIDDEN = 403, + NOT_FOUND = 404, + METHOD_NOT_ALLOWED = 405, + CONFLICT = 409, + INTERNAL_SERVER_ERROR = 500, + NOT_IMPLEMENTED = 501, + BAD_GATEWAY = 502, + SERVICE_UNAVAILABLE = 503 +} + +-- Create a response object +function http.response(status, body, headers) + return { + status = status or http.status.OK, + body = body or "", + headers = headers or {} + } +end + +-- Create JSON response +function http.json(data, status) + local json_str = moonshark.json_encode(data) + return { + status = status or http.status.OK, + body = json_str, + headers = { + ["Content-Type"] = "application/json" + } + } +end + +-- Create text response +function http.text(text, status) + return { + status = status or http.status.OK, + body = tostring(text), + headers = { + ["Content-Type"] = "text/plain" + } + } +end + +-- Create HTML response +function http.html(html, status) + return { + status = status or http.status.OK, + body = tostring(html), + headers = { + ["Content-Type"] = "text/html" + } + } +end + +-- Redirect response +function http.redirect(location, status) + return { + status = status or http.status.FOUND, + body = "", + headers = { + ["Location"] = location + } + } +end + +-- Error response +function http.error(message, status) + return http.json({ + error = message or "Internal Server Error" + }, status or http.status.INTERNAL_SERVER_ERROR) +end + +-- ====================================================================== +-- UTILITY FUNCTIONS +-- ====================================================================== + +-- Parse query string +function http.parseQuery(queryString) + local params = {} + if not queryString or queryString == "" then + return params + end + + for pair in queryString:gmatch("[^&]+") do + local key, value = pair:match("([^=]+)=?(.*)") + if key then + key = http.urlDecode(key) + value = http.urlDecode(value or "") + params[key] = value + end + end + + return params +end + +-- URL decode +function http.urlDecode(str) + if not str then return "" end + str = str:gsub("+", " ") + str = str:gsub("%%(%x%x)", function(hex) + return string.char(tonumber(hex, 16)) + end) + return str +end + +-- URL encode +function http.urlEncode(str) + if not str then return "" end + str = str:gsub("([^%w%-%.%_%~])", function(c) + return string.format("%%%02X", string.byte(c)) + end) + return str +end + +-- Parse cookies +function http.parseCookies(cookieHeader) + local cookies = {} + if not cookieHeader then return cookies end + + for pair in cookieHeader:gmatch("[^;]+") do + local key, value = pair:match("^%s*([^=]+)=?(.*)") + if key then + cookies[key:match("^%s*(.-)%s*$")] = value and value:match("^%s*(.-)%s*$") or "" + end + end + + return cookies +end + +return http \ No newline at end of file diff --git a/moonshark.go b/moonshark.go index 03c5902..5082a11 100644 --- a/moonshark.go +++ b/moonshark.go @@ -3,7 +3,9 @@ package main import ( "fmt" "os" + "os/signal" "path/filepath" + "syscall" luajit "git.sharkk.net/Sky/LuaJIT-to-Go" ) @@ -62,4 +64,38 @@ func main() { fmt.Fprintf(os.Stderr, "Error executing '%s': %v\n", scriptPath, err) os.Exit(1) } + + // Check if any servers are running to determine if we need to wait + hasRunningServers := false + if result, err := state.ExecuteWithResult(` + for i = 1, 100 do + if moonshark.http_server_is_running(i) then + return true + end + end + return false + `); err == nil { + if running, ok := result.(bool); ok && running { + hasRunningServers = true + } + } + + // Only set up signal handling if there are long-running services + if hasRunningServers { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + fmt.Printf("Script executed successfully. Press Ctrl+C to stop...\n") + + // Wait for signal + <-sigChan + fmt.Printf("\nReceived shutdown signal. Cleaning up...\n") + + // Cleanup HTTP servers through Lua + if err := state.DoString("moonshark.http_cleanup_servers()"); err != nil { + fmt.Printf("Warning: failed to cleanup servers: %v\n", err) + } + + fmt.Printf("Shutdown complete.\n") + } }