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 argv in it // 3) initiates the the "luna" module so it's possible to call Go functions // from Lua func (w *Worker) Start(argv []string, module map[string]any) error { if len(argv) == 0 { return errors.New("argv must at least contain lua file name") } w.mu.Lock() defer w.mu.Unlock() if w.started { return errors.New("already started") } w.lua.Start() defer w.lua.RestoreStackFunc()() // emulate passing arguments to the loaded chunk args := []any{} if len(argv) > 1 { for _, arg := range argv[1:] { args = append(args, arg) } } err := w.lua.PushArray(args) if err != nil { return err } w.lua.SetGlobal("arg") // register the module in the Lua context err = w.lua.PushObject(module) if err != nil { return err } w.lua.SetGlobal("luna") err = w.lua.Require(argv[0]) 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) { stringListToAny := func(slice []string) []any { res := []any{} for _, v := range slice { res = append(res, v) } return res } 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]) res := make(map[string]any) res["method"] = r.request.Method res["path"] = r.request.URL.Path fh := make(map[string]any) for k := range r.request.Header { fh[k] = r.request.Header.Get(k) } res["headers"] = fh flatQr := make(map[string]any) qr := r.request.URL.Query() for k := range qr { flatQr[k] = stringListToAny(qr[k]) } res["query"] = flatQr 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 } res["body"] = string(body) err = w.lua.PushObject(res) if err != nil { r.result <- &HTTPResponse { Code: 500, Headers: make(map[string]string), Body: "server error", } log.Println("could not form a request to lua:", err) return } err = w.lua.PCall(1, 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 }