package engine import ( "html/template" "net/http" "os" "path/filepath" "strconv" "github.com/sirupsen/logrus" "github.com/xyproto/algernon/cachemode" "github.com/xyproto/algernon/lua/codelib" "github.com/xyproto/algernon/lua/convert" "github.com/xyproto/algernon/lua/datastruct" "github.com/xyproto/algernon/lua/httpclient" "github.com/xyproto/algernon/lua/jnode" "github.com/xyproto/algernon/lua/mssql" "github.com/xyproto/algernon/lua/ollama" "github.com/xyproto/algernon/lua/onthefly" "github.com/xyproto/algernon/lua/pquery" "github.com/xyproto/algernon/lua/pure" "github.com/xyproto/algernon/lua/sqlite" "github.com/xyproto/algernon/lua/upload" "github.com/xyproto/algernon/lua/users" "github.com/xyproto/algernon/lua/webauthn" "github.com/xyproto/algernon/utils" "github.com/xyproto/gluamapper" lua "github.com/xyproto/gopher-lua" ) func setOSExit(L *lua.LState, fn lua.LGFunction) { if osTable, ok := L.GetGlobal("os").(*lua.LTable); ok { L.SetField(osTable, "exit", L.NewFunction(fn)) } } func disableOSExit(L *lua.LState) { setOSExit(L, func(L *lua.LState) int { L.RaiseError("os.exit is not allowed while serving requests") return 0 }) } func enableOSExit(L *lua.LState) { setOSExit(L, func(L *lua.LState) int { code := L.OptInt(1, 0) L.Close() os.Exit(code) return 0 }) } // LoadCommonFunctions adds most of the available Lua functions in algernon to // the given Lua state struct func (ac *Config) LoadCommonFunctions(w http.ResponseWriter, req *http.Request, filename string, L *lua.LState, flushFunc func(), httpStatus *FutureStatus) { disableOSExit(L) // Make basic functions, like print, available to the Lua script. // Only exports functions that can relate to HTTP responses or requests. ac.LoadBasicWeb(w, req, L, filename, flushFunc, httpStatus) // Make other basic functions available ac.LoadBasicSystemFunctions(L) // Functions for rendering markdown or amber ac.LoadRenderFunctions(w, req, L) // If there is a database backend if ac.perm != nil { // Retrieve the userstate userstate := ac.perm.UserState() // Set the cookie secret, if set if ac.cookieSecret != "" { userstate.SetCookieSecret(ac.cookieSecret) } // Functions for serving files in the same directory as a script ac.LoadServeFile(w, req, L, filename) // Functions mainly for adding admin prefixes and configuring permissions ac.LoadServerConfigFunctions(L, filename) // Make the functions related to userstate available to the Lua script users.Load(w, req, L, userstate) // Make WebAuthn functions available to the Lua script webauthn.Load(w, req, L, userstate) creator := userstate.Creator() // Simpleredis data structures datastruct.LoadList(L, creator) datastruct.LoadSet(L, creator) datastruct.LoadHash(L, creator) datastruct.LoadKeyValue(L, creator) // For saving and loading Lua functions codelib.Load(L, creator) // For executing PostgreSQL queries pquery.Load(L) // For executing MSSQL queries mssql.Load(L) // For executing SQLite queries sqlite.Load(L) } // For handling JSON data jnode.LoadJSONFunctions(L) ac.LoadJFile(L, filepath.Dir(filename)) jnode.Load(L) // Extras pure.Load(L) // pprint // exportREPL(L) // Plugins ac.LoadPluginFunctions(L, nil) // Cache ac.LoadCacheFunctions(L) // Pages and Tags onthefly.Load(L) // Ollama / LLM support ollama.Load(L) // File uploads upload.Load(L, w, req, filepath.Dir(filename)) // HTTP Client httpclient.Load(L, ac.serverHeaderName) } // RunLua uses a Lua file as the HTTP handler. Also has access to the userstate // and permissions. Returns an error if there was a problem with running the lua // script, otherwise nil. func (ac *Config) RunLua(w http.ResponseWriter, req *http.Request, filename string, flushFunc func(), fust *FutureStatus) error { // Retrieve a Lua state L := ac.luapool.Borrow() defer ac.luapool.Return(L) // Warn if the connection is closed before the script has finished. if ac.verboseMode { done := make(chan bool, 1) // Stop the background goroutine when this function returns. defer func() { done <- true }() // Set up a background notifier go func() { ctx := req.Context() select { case <-ctx.Done(): // Client is done logrus.Warn("Connection to client closed") case <-done: // We are done } }() // Call the goroutine } // Export functions to the Lua state // Flush can be an uninitialized channel, it is handled in the function. ac.LoadCommonFunctions(w, req, filename, L, flushFunc, fust) // Run the script and return the error value. // Logging and/or HTTP response is handled elsewhere. if filepath.Ext(filename) == ".tl" { return L.DoString(` local fname = [[` + filename + `]] local do_cache = ` + strconv.FormatBool(ac.cacheMode == cachemode.Production) + ` if do_cache and tl.cache[fname] then tl.cache[fname]() return end local result, err = tl.process(fname) if err ~= nil then throw('Teal failed to process file: '..err) end if #result.syntax_errors > 0 then local err = result.syntax_errors[1] throw(err.filename..':'..err.y..': Teal processing error: '..err.msg, 0) end local code, gen_error = tl.pretty_print_ast(result.ast, "5.1") if gen_error ~= nil then throw('Teal failed to generate Lua: '..err) end local chunk = load(code) if do_cache then tl.cache[fname] = chunk end chunk() `) } return L.DoFile(filename) } // RunConfiguration runs a Lua file as a configuration script. Also has access // to the userstate and permissions. Returns an error if there was a problem // with running the lua script, otherwise nil. perm can be nil, but then several // Lua functions will not be exposed. // // The idea is not to change the Lua struct or the luapool, but to set the // configuration variables with the given Lua configuration script. // // luaHandler is a flag that lets Lua functions like "handle" and "servedir" be available or not. func (ac *Config) RunConfiguration(filename string, mux *http.ServeMux, withHandlerFunctions bool) error { // Retrieve a Lua state L := ac.luapool.Borrow() // Basic system functions, like log() ac.LoadBasicSystemFunctions(L) // Server settings functions (available regardless of database backend) ac.loadServerSettingsFunctions(L, filename) // If there is a database backend if ac.perm != nil { // Retrieve the userstate userstate := ac.perm.UserState() // Server configuration functions (permission-related) ac.LoadServerConfigFunctions(L, filename) creator := userstate.Creator() // Simpleredis data structures (could be used for storing server stats) datastruct.LoadList(L, creator) datastruct.LoadSet(L, creator) datastruct.LoadHash(L, creator) datastruct.LoadKeyValue(L, creator) // For saving and loading Lua functions codelib.Load(L, creator) // For executing PostgreSQL queries pquery.Load(L) // For executing MSSQL queries mssql.Load(L) // For executing SQLite queries sqlite.Load(L) } // For handling JSON data jnode.LoadJSONFunctions(L) ac.LoadJFile(L, filepath.Dir(filename)) jnode.Load(L) // Extras pure.Load(L) // Plugins ac.LoadPluginFunctions(L, nil) // Cache ac.LoadCacheFunctions(L) // Pages and Tags onthefly.Load(L) // Ollama / LLM support ollama.Load(L) // HTTP Client httpclient.Load(L, ac.serverHeaderName) if withHandlerFunctions { // Lua HTTP handlers ac.LoadLuaHandlerFunctions(L, filename, mux, false, nil, ac.defaultTheme, true) } // Run the script if err := L.DoFile(filename); err != nil { // Close the Lua state L.Close() // Logging and/or HTTP response is handled elsewhere return err } // Only put the Lua state back if there were no errors ac.luapool.Return(L) // Populate a pool of Lua states dedicated to serving handle() requests, // so that concurrent requests can execute on different states in parallel. if withHandlerFunctions { if err := ac.buildHandlerPool(filename, mux); err != nil { return err } } return nil } // loadServerConfigNoopFunctions binds the server configuration functions as // no-ops. Used while populating the handler pool: the user's script will call // functions like SetAddr or AddReverseProxy during setup, but those effects // have already been applied on the main state and must not run again per // pool state. func (ac *Config) loadServerConfigNoopFunctions(L *lua.LState) { noop := L.NewFunction(func(_ *lua.LState) int { return 0 }) noopTrue := L.NewFunction(func(L *lua.LState) int { L.Push(lua.LBool(true)) return 1 }) cookieSecret := L.NewFunction(func(L *lua.LState) int { L.Push(lua.LString(ac.cookieSecret)) return 1 }) serverInfo := L.NewFunction(func(L *lua.LState) int { L.Push(lua.LString(ac.Info())) return 1 }) for _, name := range []string{ "SetAddr", "SetHTTPAddr", "SetHTTPSAddr", "SetPorts", "SetRedirect", "SetLetsEncrypt", "SetInteractive", "SetDirBaseURL", "SetCookieSecret", "ClearPermissions", "AddUserPrefix", "AddAdminPrefix", "AddReverseProxy", "DenyHandler", "OnReady", } { L.SetGlobal(name, noop) } for _, name := range []string{"LogTo", "ServerFile", "ServerDir"} { L.SetGlobal(name, noopTrue) } L.SetGlobal("CookieSecret", cookieSecret) L.SetGlobal("ServerInfo", serverInfo) } // loadPoolStateFunctions binds every function the user's script needs to run // inside a pool state. Request-scoped functions (LoadCommonFunctions) are // rebound on every request, so here we only cover what the setup phase uses. func (ac *Config) loadPoolStateFunctions(L *lua.LState, filename string, mux *http.ServeMux) { disableOSExit(L) ac.LoadBasicSystemFunctions(L) ac.loadServerConfigNoopFunctions(L) if ac.perm != nil { userstate := ac.perm.UserState() creator := userstate.Creator() datastruct.LoadList(L, creator) datastruct.LoadSet(L, creator) datastruct.LoadHash(L, creator) datastruct.LoadKeyValue(L, creator) codelib.Load(L, creator) pquery.Load(L) mssql.Load(L) sqlite.Load(L) } jnode.LoadJSONFunctions(L) ac.LoadJFile(L, filepath.Dir(filename)) jnode.Load(L) pure.Load(L) ac.LoadPluginFunctions(L, nil) ac.LoadCacheFunctions(L) onthefly.Load(L) ollama.Load(L) httpclient.Load(L, ac.serverHeaderName) ac.LoadLuaHandlerFunctions(L, filename, mux, false, nil, ac.defaultTheme, false) } // buildHandlerPool creates ac.handlerPoolSize fresh Lua states, runs the // script in each, and enqueues them in ac.handlerPool. Every state ends up // with its own copy of each handle() function stored in its Lua registry. func (ac *Config) buildHandlerPool(filename string, mux *http.ServeMux) error { size := max(ac.handlerPoolSize, 1) pool := newHandlerPool(size) for i := range size { L := lua.NewState() ac.loadPoolStateFunctions(L, filename, mux) if err := L.DoFile(filename); err != nil { L.Close() if len(pool.states) == 0 { // Couldn't build any usable pool state return err } logrus.Errorf("handler pool state %d failed to initialise: %v", i, err) continue } pool.Add(L) } ac.handlerPool = pool AtShutdown(func() { pool.Close() }) return nil } // The global tables that are provided by Lua itself, or by Teal var luaLibraries = map[string]bool{"_G": true, "bit32": true, "channel": true, "coroutine": true, "debug": true, "io": true, "math": true, "os": true, "package": true, "string": true, "table": true, "tl": true} // gluamapperOption keeps the original key names when converting Lua tables. var gluamapperOption = gluamapper.Option{ NameFunc: func(s string) string { return s }, } // LuaFunctionMap returns the functions available in the given Lua code as // functions in a map that can be used by templates. // // Note that the lua functions must only accept and return strings // and that only the first returned value will be accessible. // The Lua functions may take an optional number of arguments. func (ac *Config) LuaFunctionMap(w http.ResponseWriter, req *http.Request, luadata []byte, filename string) (template.FuncMap, error) { // Retrieve a Lua state L := ac.luapool.Borrow() defer ac.luapool.Return(L) // Prepare an empty map of functions (and variables) funcs := make(template.FuncMap) // Give no filename (an empty string will be handled correctly by the function). ac.LoadCommonFunctions(w, req, filename, L, nil, nil) // Run the script if err := L.DoString(string(luadata)); err != nil { // Close the Lua state L.Close() // Logging and/or HTTP response is handled elsewhere return funcs, err } // Extract the available functions from the Lua state globalTable := L.G.Global globalTable.ForEach(func(key, value lua.LValue) { // Check if the current value is a string variable if luaString, ok := value.(lua.LString); ok { // Store the variable in the same map as the functions (string -> interface) // for ease of use together with templates. funcs[key.String()] = luaString.String() } else if luaTable, ok := value.(*lua.LTable); ok { // Skip the Lua libraries, they are not meant for the templates // and can be both large and self-referential if luaLibraries[key.String()] { return } if convert.ContainsTable(luaTable) { // Tables within tables, see issue #119 funcs[key.String()] = gluamapper.ToGoValue(luaTable, gluamapperOption) } else { // Convert the table to a map and save it. // Ignore values of a different type. mapinterface, _ := convert.Table2map(luaTable, false) switch m := mapinterface.(type) { case map[string]string: funcs[key.String()] = map[string]string(m) case map[string]int: funcs[key.String()] = map[string]int(m) case map[int]string: funcs[key.String()] = map[int]string(m) case map[int]int: funcs[key.String()] = map[int]int(m) } } // Check if the current value is a function } else if luaFunc, ok := value.(*lua.LFunction); ok { // Only export the functions defined in the given Lua code, // not all the global functions. IsG is true if the function is global. if !luaFunc.IsG { functionName := key.String() // Register the function, with a variable number of string arguments // Functions returning (string, error) are supported by html.template funcs[functionName] = func(args ...string) (any, error) { // Create a brand new Lua state L2 := ac.luapool.New() defer L2.Close() // Set up a new Lua state with the current http.ResponseWriter and *http.Request ac.LoadCommonFunctions(w, req, filename, L2, nil, nil) // Push the Lua function to run L2.Push(luaFunc) // Push the given arguments for _, arg := range args { L2.Push(lua.LString(arg)) } // Run the Lua function err := L2.PCall(len(args), lua.MultRet, nil) if err != nil { // If calling the function did not work out, return the infostring and error return utils.Infostring(functionName, args), err } // Empty return value if no values were returned var retval any // Return the first of the returned arguments, as a string if L2.GetTop() >= 1 { lv := L2.Get(-1) tbl, isTable := lv.(*lua.LTable) switch { case isTable: // lv was a Lua Table retval = gluamapper.ToGoValue(tbl, gluamapperOption) if ac.debugMode && ac.verboseMode { logrus.Infof("%s -> (map)", utils.Infostring(functionName, args)) } case lv.Type() == lua.LTString: // lv is a Lua String retstr := L2.ToString(1) retval = retstr if ac.debugMode && ac.verboseMode { logrus.Infof("%s -> %q", utils.Infostring(functionName, args), retstr) } case lv.Type() == lua.LTNumber: // lv is a Lua Number retnum := L2.ToNumber(1) retval = retnum if ac.debugMode && ac.verboseMode { logrus.Infof("%s -> %v", utils.Infostring(functionName, args), retnum) } default: retval = "!! not a table, string or number !!" logrus.Warnf("The returned value from %s has an unrecognized type: %v", utils.Infostring(functionName, args), lv.Type()) } } // No return value, return an empty string and nil return retval, nil } } } }) // Return the map of functions return funcs, nil }