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
}