package main import ( "errors" "io" "log" "net/http" "sync" ) type HTTPRequest struct { request *http.Request route string debug bool result chan *HTTPResponse } type HTTPResponse struct { Code int Headers map[string]string Body string } type Worker struct { lua *Lua routes map[string]LuaRef started bool mu sync.Mutex } func HandleHTTPRequest( queue chan any, route string, req *http.Request, ) chan *HTTPResponse { res := make(chan *HTTPResponse) queue <- &HTTPRequest{ request: req, route: route, result: res, } return res } // NewWorker creates a new instance of Worker type. func NewWorker() *Worker { return &Worker { routes: make(map[string]LuaRef), lua: &Lua{}, } } // Start starts the worker: // 1) creates a Lua context // 2) executes the filename in it // 3) initiates the the "luna" module so it's possible to call Go functions // from Lua func (w *Worker) Start(filename string, module map[string]any) error { w.mu.Lock() defer w.mu.Unlock() if w.started { return errors.New("already started") } w.lua.Start() defer w.lua.RestoreStackFunc()() w.initLunaModule(module) err := w.lua.Require(filename) if err != nil { return err } w.started = true return nil } // Listen starts a goroutine listening/handling HTTP requests from the queue. func (w *Worker) Listen(queue chan any) { handle := func() { defer w.lua.RestoreStackFunc()() r := <- queue switch r.(type) { case *HTTPRequest: r := r.(*HTTPRequest) if _, ok := w.routes[r.route]; !ok { r.result <- &HTTPResponse { Code: 404, Headers: make(map[string]string), Body: "not found", } log.Println("no corresponding route") return } w.lua.PushFromRef(w.routes[r.route]) w.lua.PushString(r.request.Method) w.lua.PushString(r.request.URL.Path) fh := make(map[string]string) for k := range r.request.Header { fh[k] = r.request.Header.Get(k) } w.lua.PushStringTable(fh) body, err := io.ReadAll(r.request.Body) if err != nil { r.result <- &HTTPResponse { Code: 500, Headers: make(map[string]string), Body: "server error", } log.Println("could not read a request body:", err) return } w.lua.PushString(string(body)) err = w.lua.PCall(4, 3) if err != nil { r.result <- &HTTPResponse { Code: 500, Headers: make(map[string]string), Body: "server error", } log.Println("could not read a request body:", err) return } code := w.lua.ToInt(-3) rbody := w.lua.ToString(-1) // Parse headers. headers := make(map[string]string) w.lua.Pop(1) w.lua.PushNil() for w.lua.Next() { if !w.lua.IsString(-2) || !w.lua.IsString(-2) { w.lua.Pop(1) continue } v := w.lua.ToString(-1) w.lua.Pop(1) // We must not pop the item key from the stack // because otherwise C.lua_next won't work // properly. k := w.lua.ToString(-1) headers[k] = v } r.result <- &HTTPResponse { Code: int(code), Headers: headers, Body: rbody, } default: log.Fatal("unknown request") } } for { handle() } } // Eval evaluates the code in the Lua context. func (w *Worker) Eval(code string) error { w.mu.Lock() defer w.mu.Unlock() defer w.lua.RestoreStackFunc()() return w.lua.LoadString(code) } // SetRoute sets a handler for the route. func (w *Worker) SetRoute(route string, handler LuaRef) { w.routes[route] = handler } // Stop stops the worker closing the Lua context. TODO: stop Listen goroutine // as well. func (w *Worker) Stop() { w.mu.Lock() defer w.mu.Unlock() w.lua.Close() } // HasSameLua checks if the Lua context belongs to the worker. func (w *Worker) HasSameLua(l *Lua) bool { return w.lua == l } // initLunaModule registers the module in the Lua context. func (w *Worker) initLunaModule(module map[string]any) { var pushTable func(t map[string]any) pushTable = func (t map[string]any) { w.lua.CreateTable(len(t)) for k, v := range t { switch v.(type) { case string: v, _ := v.(string) w.lua.PushString(v) case func (l *Lua) int: v, _ := v.(func (l *Lua) int) w.lua.PushGoFunction(v) case int: v, _ := v.(int) w.lua.PushNumber(v) case map[string]any: v, _ := v.(map[string]any) pushTable(v) default: // FIXME: more details. log.Fatal("unsupported module value type") } w.lua.SetTableItem(k) } } pushTable(module) w.lua.SetGlobal("luna") }