engine/serverconf.go
Rama main · réplica de lectura del repositorio original.
package engine
import (
"errors"
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"github.com/sirupsen/logrus"
"github.com/xyproto/algernon/utils"
lua "github.com/xyproto/gopher-lua"
bolt "github.com/xyproto/permissionbolt/v2"
redis "github.com/xyproto/permissions"
mariadb "github.com/xyproto/permissionsql/v2"
sqlite "github.com/xyproto/permissionsqlite_noc"
"github.com/xyproto/pinterface/v2"
postgres "github.com/xyproto/pstore"
"github.com/xyproto/simpleredis/v2"
)
// Info returns a string with various info about the current configuration
func (ac *Config) Info() string {
var sb strings.Builder
if !ac.singleFileMode {
sb.WriteString("Server directory:\t" + ac.serverDirOrFilename + "\n")
} else {
sb.WriteString("Filename:\t\t" + ac.serverDirOrFilename + "\n")
}
if ac.dbName == "" {
sb.WriteString("Database:\t\tDisabled\n")
} else {
sb.WriteString("Database:\t\t" + ac.dbName + "\n")
}
if ac.luaServerFilename != "" {
sb.WriteString("Server filename:\t" + ac.luaServerFilename + "\n")
}
// Write the status of flags that can be toggled
utils.WriteStatus(&sb, "Options", map[string]bool{
"Debug": ac.debugMode,
"Production": ac.productionMode,
"Auto-refresh": ac.autoRefresh,
"Dev": ac.devMode,
"NonInteractive": ac.serverMode,
"StatCache": ac.cacheFileStat,
})
sb.WriteString("Cache mode:\t\t" + ac.cacheMode.String() + "\n")
if ac.cacheSize != 0 {
sb.WriteString(fmt.Sprintf("Cache size:\t\t%d bytes\n", ac.cacheSize))
}
if ac.serverLogFile != "" {
sb.WriteString("Log file:\t\t" + ac.serverLogFile + "\n")
}
if !(ac.serveJustHTTP2 || ac.serveJustHTTP) {
sb.WriteString("TLS certificate:\t" + ac.serve.serverCert + "\n")
sb.WriteString("TLS key:\t\t" + ac.serve.serverKey + "\n")
}
if ac.autoRefresh {
if ac.separateEventServer {
sb.WriteString("Event server:\t\t" + ac.eventAddr + "\n")
} else {
sb.WriteString("Event path:\t\t" + ac.defaultEventPath + " (on main server)\n")
}
}
if ac.autoRefreshDir != "" {
sb.WriteString("Only watching:\t\t" + ac.autoRefreshDir + "\n")
}
if ac.redisAddr != ac.defaultRedisColonPort {
sb.WriteString("Redis address:\t\t" + ac.redisAddr + "\n")
}
if ac.disableRateLimiting {
sb.WriteString("Request limit:\t\tOff\n")
} else {
sb.WriteString(fmt.Sprintf("Request limit:\t\t%d/sec per visitor\n", ac.limitRequests))
}
if ac.redisDBindex != 0 {
sb.WriteString(fmt.Sprintf("Redis database index:\t%d\n", ac.redisDBindex))
}
if ac.largeFileSize > 0 {
sb.WriteString(fmt.Sprintf("Large file threshold:\t%v bytes\n", ac.largeFileSize))
}
if ac.writeTimeout > 0 {
sb.WriteString(fmt.Sprintf("Large file timeout:\t%v sec\n", ac.writeTimeout))
}
if len(ac.serverConfigurationFilenames) > 0 {
sb.WriteString(fmt.Sprintf("Server configuration:\t%v\n", ac.serverConfigurationFilenames))
}
if ac.internalLogFilename != os.DevNull {
sb.WriteString("Internal log file:\t" + ac.internalLogFilename + "\n")
}
sb.WriteString("Listening for:\t\t" + ac.listeningSummary())
return strings.TrimSpace(sb.String())
}
// listeningSummary returns all configured servers/listeners as a table
func (ac *Config) listeningSummary() string {
type row struct{ proto, host, port, tls string }
var rows []row
add := func(protocol, addr string, tls bool) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
host, port = addr, ""
}
if host == "" {
host = "localhost"
}
tlsStr := "plain"
if tls {
tlsStr = "TLS"
}
rows = append(rows, row{protocolName(protocol), host, port, tlsStr})
}
switch {
case len(ac.serve.portSettings) > 0:
for _, ps := range ac.serve.portSettings {
add(ps.Protocol, ps.Addr, ps.TLS)
}
case ac.serve.httpAddr != "" || ac.serve.httpsAddr != "":
if ac.serve.httpAddr != "" {
add("http", ac.serve.httpAddr, false)
}
if ac.serve.httpsAddr != "" {
add("http2", ac.serve.httpsAddr, true)
}
case ac.productionMode:
add("http", utils.JoinHostPort(ac.serverHost, ":80"), false)
add("http2", utils.JoinHostPort(ac.serverHost, ":443"), true)
case ac.serveJustHTTP:
add("http", ac.serverAddr, false)
case ac.serveJustHTTP2:
add("http2", ac.serverAddr, false)
default:
add("http2", ac.serverAddr, true)
}
if len(rows) == 0 {
return "nothing"
}
var pW, hW, portW int
for _, r := range rows {
if n := len(r.proto); n > pW {
pW = n
}
if n := len(r.host); n > hW {
hW = n
}
if n := len(r.port); n > portW {
portW = n
}
}
var sb strings.Builder
for i, r := range rows {
if i > 0 {
sb.WriteString("\t\t\t")
}
fmt.Fprintf(&sb, "%-*s %-*s %*s %s\n", pW, r.proto, hW, r.host, portW, r.port, r.tls)
}
return sb.String()
}
// protocolName returns the short name used in the listener table.
func protocolName(protocol string) string {
switch protocol {
case "http":
return "HTTP"
case "http2":
return "HTTP/2"
case "http3", "quic":
return "HTTP/3"
case "event":
return "SSE"
}
return strings.ToUpper(protocol)
}
// LoadServerConfigFunctions makes functions related to server configuration and
// permissions available to the given Lua struct.
func (ac *Config) LoadServerConfigFunctions(L *lua.LState, filename string) error {
if ac.perm == nil {
return errors.New("perm is nil when loading server config functions")
}
// Clear the default path prefixes. This makes everything public.
L.SetGlobal("ClearPermissions", L.NewFunction(func(_ *lua.LState) int {
ac.perm.Clear()
return 0 // number of results
}))
// Registers a path prefix, for instance "/secret",
// as having *user* rights.
L.SetGlobal("AddUserPrefix", L.NewFunction(func(L *lua.LState) int {
path := L.ToString(1)
ac.perm.AddUserPath(path)
return 0 // number of results
}))
// Registers a path prefix, for instance "/secret",
// as having *admin* rights.
L.SetGlobal("AddAdminPrefix", L.NewFunction(func(L *lua.LState) int {
path := L.ToString(1)
ac.perm.AddAdminPath(path)
return 0 // number of results
}))
// Sets a Lua function as a custom "permissions denied" page handler.
L.SetGlobal("DenyHandler", L.NewFunction(func(L *lua.LState) int {
luaDenyFunc := L.ToFunction(1)
// L is shared by all requests, and a Lua state is not goroutine-safe
var denyMut sync.Mutex
// Custom handler for when permissions are denied
ac.perm.SetDenyFunction(func(w http.ResponseWriter, req *http.Request) {
denyMut.Lock()
defer denyMut.Unlock()
fw := &forbiddenWriter{ResponseWriter: w}
defer fw.finish()
w = fw
// Set up a new Lua state with the current http.ResponseWriter and *http.Request, without caching
ac.LoadCommonFunctions(w, req, filename, L, nil, nil)
// Then run the given Lua function
L.Push(luaDenyFunc)
if err := L.PCall(0, lua.MultRet, nil); err != nil {
// Non-fatal error
logrus.Error("Permission denied handler failed:", err)
// Use the default permission handler from now on if the lua function fails
ac.perm.SetDenyFunction(redis.PermissionDenied)
ac.perm.DenyFunction()(w, req)
}
})
return 0 // number of results
}))
return nil
}
// loadServerSettingsFunctions registers Lua functions for server settings
// that do not require a database backend or permissions.
func (ac *Config) loadServerSettingsFunctions(L *lua.LState, filename string) {
// Set a default host and port. Maybe useful for alg applications.
L.SetGlobal("SetAddr", L.NewFunction(func(L *lua.LState) int {
ac.serverAddrLua = L.ToString(1)
return 0 // number of results
}))
// Set the HTTP (non-TLS) listen address (only if not already set by flags/positional)
L.SetGlobal("SetHTTPAddr", L.NewFunction(func(L *lua.LState) int {
if !ac.serve.portConfigFromCLI {
ac.serve.httpAddr = L.ToString(1)
}
return 0 // number of results
}))
// Set the HTTPS (TLS) listen address (only if not already set by flags/positional)
L.SetGlobal("SetHTTPSAddr", L.NewFunction(func(L *lua.LState) int {
if !ac.serve.portConfigFromCLI {
ac.serve.httpsAddr = L.ToString(1)
}
return 0 // number of results
}))
// Enable or disable HTTP to HTTPS redirect (only if not already set by flags)
L.SetGlobal("SetRedirect", L.NewFunction(func(L *lua.LState) int {
if !ac.serve.redirectFromCLI {
ac.serve.redirectHTTP = bool(L.ToBool(1))
}
return 0 // number of results
}))
// Enable or disable Let's Encrypt / CertMagic (only if not already set by flags)
L.SetGlobal("SetLetsEncrypt", L.NewFunction(func(L *lua.LState) int {
if !ac.serve.certMagicFromCLI {
ac.serve.useCertMagic = bool(L.ToBool(1))
}
return 0 // number of results
}))
// Enable or disable interactive mode (only if not already set by flags)
L.SetGlobal("SetInteractive", L.NewFunction(func(L *lua.LState) int {
if !ac.serverModeFromCLI {
ac.serverMode = !bool(L.ToBool(1))
}
return 0 // number of results
}))
// Set a base URL for the links in the directory listing, unless it was
// already set with --dirbaseurl
L.SetGlobal("SetDirBaseURL", L.NewFunction(func(L *lua.LState) int {
if ac.dirBaseURL == "" {
ac.dirBaseURL = L.ToString(1)
}
return 0 // number of results
}))
// Configure listeners with full control over protocol, port and TLS.
// Takes a table of tables: SetPorts{{":8080","http",false},{":8443","http2",true}}
// Only applies if port configuration was not already set by flags or positional args.
L.SetGlobal("SetPorts", L.NewFunction(func(L *lua.LState) int {
if ac.serve.portConfigFromCLI {
return 0
}
tbl := L.ToTable(1)
if tbl == nil {
logrus.Error("SetPorts: expected a table argument")
return 0
}
var settings []PortSetting
tbl.ForEach(func(_ lua.LValue, value lua.LValue) {
entry, ok := value.(*lua.LTable)
if !ok {
return
}
var ps PortSetting
// Support both positional {addr, protocol, tls} and named {addr=, protocol=, tls=}
if addrVal := entry.RawGetString("addr"); addrVal != lua.LNil {
ps.Addr = addrVal.String()
} else if addrVal := entry.RawGetInt(1); addrVal != lua.LNil {
ps.Addr = addrVal.String()
}
if protoVal := entry.RawGetString("protocol"); protoVal != lua.LNil {
ps.Protocol = protoVal.String()
} else if protoVal := entry.RawGetInt(2); protoVal != lua.LNil {
ps.Protocol = protoVal.String()
}
if tlsVal := entry.RawGetString("tls"); tlsVal != lua.LNil {
ps.TLS = lua.LVAsBool(tlsVal)
} else if tlsVal := entry.RawGetInt(3); tlsVal != lua.LNil {
ps.TLS = lua.LVAsBool(tlsVal)
}
// Normalize "quic" to "http3"
if ps.Protocol == "quic" {
ps.Protocol = "http3"
}
// Warn about HTTP/3 without TLS
if ps.Protocol == "http3" && !ps.TLS {
logrus.Warn("HTTP/3 without TLS is non-standard and not supported by browsers")
}
settings = append(settings, ps)
})
ac.serve.portSettings = settings
return 0 // number of results
}))
// Set the default cookie secret. This is for the server config, before
// the userstate has been instantiated.
L.SetGlobal("SetCookieSecret", L.NewFunction(func(L *lua.LState) int {
ac.cookieSecret = L.ToString(1)
return 0 // number of results
}))
// Get the default cookie secret. This is for the server config, before
// the userstate has been instantiated.
L.SetGlobal("CookieSecret", L.NewFunction(func(L *lua.LState) int {
L.Push(lua.LString(ac.cookieSecret))
return 1 // number of results
}))
// Add a new reverse proxy given a: path prefix, endpoint and endpoint URL
L.SetGlobal("AddReverseProxy", L.NewFunction(func(L *lua.LState) int {
rp, err := NewReverseProxy(L.ToString(1), L.ToString(2))
if err != nil {
logrus.Errorf("AddReverseProxy: %v", err)
return 0 // number of results
}
if ac.reverseProxyConfig == nil {
ac.reverseProxyConfig = NewReverseProxyConfig()
}
ac.reverseProxyConfig.Add(rp)
return 0 // number of results
}))
// Sets a Lua function to be run once the server is done parsing configuration and arguments.
L.SetGlobal("OnReady", L.NewFunction(func(L *lua.LState) int {
luaReadyFunc := L.ToFunction(1)
// Custom handler for when permissions are denied.
// Put the *lua.LState in a closure.
ac.serverReadyFunctionLua = func() {
// Run the given Lua function
L.Push(luaReadyFunc)
if err := L.PCall(0, lua.MultRet, nil); err != nil {
// Non-fatal error
logrus.Error("The OnReady function failed:", err)
}
}
return 0 // number of results
}))
// Set a access log filename. If blank, the log will go to the console (or browser, if debug mode is set).
L.SetGlobal("LogTo", L.NewFunction(func(L *lua.LState) int {
filename := L.ToString(1)
ac.serverLogFile = filename
// Log as JSON by default
logrus.SetFormatter(&logrus.JSONFormatter{})
// Log to stderr if an empty filename is given
if filename == "" {
logrus.SetOutput(os.Stderr)
L.Push(lua.LBool(true))
return 1 // number of results
}
// Try opening/creating the given filename, for appending
lw, err := openLogWriter(filename, defaultLogPermissions)
if err != nil {
logrus.Error(err)
L.Push(lua.LBool(false))
return 1 // number of results
}
// Set the file to log to and return
if ac.serverLog != nil {
ac.serverLog.Close()
}
ac.serverLog = lw
logrus.SetOutput(lw)
L.Push(lua.LBool(true))
return 1 // number of results
}))
// Use a single Lua file as the server, instead of directory structure
L.SetGlobal("ServerFile", L.NewFunction(func(L *lua.LState) int {
givenFilename := L.ToString(1)
serverFilename := filepath.Join(filepath.Dir(filename), givenFilename)
if !ac.fs.Exists(serverFilename) {
logrus.Error("Could not find", serverFilename)
L.Push(lua.LBool(false))
return 1 // number of results
}
ac.luaServerFilename = serverFilename
L.Push(lua.LBool(true))
return 1 // number of results
}))
// Set the server directory
L.SetGlobal("ServerDir", L.NewFunction(func(L *lua.LState) int {
givenDirectory := L.ToString(1)
if !ac.fs.Exists(givenDirectory) {
logrus.Error("Could not find", givenDirectory)
L.Push(lua.LBool(false))
return 1 // number of results
}
ac.serverDirOrFilename = filepath.Clean(givenDirectory)
L.Push(lua.LBool(true))
return 1 // number of results
}))
L.SetGlobal("ServerInfo", L.NewFunction(func(L *lua.LState) int {
// Return the string, but drop the final newline
L.Push(lua.LString(ac.Info()))
return 1 // number of results
}))
}
// DatabaseBackend tries to retrieve a database backend, using one of the
// available permission middleware packages. It assign a name to dbName
// (used for the status output) and returns a IPermissions struct.
func (ac *Config) DatabaseBackend() (pinterface.IPermissions, error) {
var (
err error
perm pinterface.IPermissions
)
// If Bolt is to be used and no filename is given
if ac.useBolt && (ac.boltFilename == "") {
ac.boltFilename = ac.defaultBoltFilename
}
if ac.boltFilename != "" {
// New permissions middleware, using a Bolt database
perm, err = bolt.NewWithConf(ac.boltFilename)
if err != nil {
if err.Error() == "timeout" {
tempFile, errTemp := os.CreateTemp("", "algernon")
if errTemp != nil {
logrus.Fatal("Unable to find a temporary file to use:", errTemp)
}
ac.boltFilename = tempFile.Name() + ".db"
} else {
logrus.Errorf("Could not use Bolt as database backend: %s", err)
}
} else {
ac.dbName = "Bolt (" + ac.boltFilename + ")"
}
// Try the new database filename if there was a timeout
if ac.dbName == "" && ac.boltFilename != ac.defaultBoltFilename {
perm, err = bolt.NewWithConf(ac.boltFilename)
if err != nil {
if err.Error() == "timeout" {
logrus.Error("The Bolt database timed out!")
} else {
logrus.Errorf("Could not use Bolt as database backend: %s", err)
}
} else {
ac.dbName = "Bolt, temporary"
}
}
}
if ac.dbName == "" && ac.mariadbDSN != "" {
// New permissions middleware, using a MariaDB/MySQL database
perm, err = mariadb.NewWithDSN(ac.mariadbDSN, ac.mariaDatabase)
if err != nil {
logrus.Errorf("Could not use MariaDB/MySQL as database backend: %s", err)
} else {
// The connection string may contain a password, so don't include it in the dbName
ac.dbName = "MariaDB/MySQL"
}
}
if ac.dbName == "" && ac.sqliteConnectionString != "" {
// example connection string: sqlite.db&cache=shared&mode=memory
perm, err = sqlite.NewWithConf(ac.sqliteConnectionString)
if err != nil {
logrus.Errorf("Could not use SQLite as database backend: %s", err)
} else {
// The connection string may contain a password, so don't include it in the dbName
ac.dbName = "SQLite"
}
}
if ac.dbName == "" && ac.mariaDatabase != "" {
// Given a database, but not a host, connect to localhost
// New permissions middleware, using a MariaDB/MySQL database
perm, err = mariadb.NewWithConf("test:@127.0.0.1/" + ac.mariaDatabase)
if err != nil {
if ac.mariaDatabase != "" {
logrus.Errorf("Could not use MariaDB/MySQL as database backend: %s", err)
} else {
logrus.Warnf("Could not use MariaDB/MySQL as database backend: %s", err)
}
} else {
// The connection string may contain a password, so don't include it in the dbName
ac.dbName = "MariaDB/MySQL"
}
}
if ac.dbName == "" && ac.postgresDSN != "" {
// New permissions middleware, using a PostgreSQL database
perm, err = postgres.NewWithDSN(ac.postgresDSN, ac.postgresDatabase)
if err != nil {
logrus.Errorf("Could not use PostgreSQL as database backend: %s", err)
} else {
// The connection string may contain a password, so don't include it in the dbName
ac.dbName = "PostgreSQL"
}
}
if ac.dbName == "" && ac.postgresDatabase != "" {
// Given a database, but not a host, connect to localhost
// New permissions middleware, using a PostgreSQL database
perm, err = postgres.NewWithConf("postgres:@127.0.0.1/" + ac.postgresDatabase)
if err != nil {
if ac.postgresDatabase != "" {
logrus.Errorf("Could not use PostgreSQL as database backend: %s", err)
} else {
logrus.Warnf("Could not use PostgreSQL as database backend: %s", err)
}
} else {
// The connection string may contain a password, so don't include it in the dbName
ac.dbName = "PostgreSQL"
}
}
if ac.dbName == "" && ac.redisAddrSpecified {
// New permissions middleware, using a Redis database
logrus.Info("Testing redis connection")
if err := simpleredis.TestConnectionHost(ac.redisAddr); err != nil {
logrus.Info("Redis connection failed")
// Only output an error when a Redis host other than the default host+port was specified
if ac.singleFileMode {
logrus.Warnf("Could not use Redis as database backend: %s", err)
} else {
logrus.Errorf("Could not use Redis as database backend: %s", err)
}
} else {
logrus.Info("Redis connection worked out")
var err error
logrus.Info("Connecting to Redis...")
perm, err = redis.NewWithRedisConf2(ac.redisDBindex, ac.redisAddr)
if err != nil {
logrus.Warnf("Could not use Redis as database backend: %s", err)
} else {
ac.dbName = "Redis"
}
}
}
if ac.dbName == "" && ac.boltFilename == "" {
ac.boltFilename = ac.defaultBoltFilename
perm, err = bolt.NewWithConf(ac.boltFilename)
if err != nil {
if err.Error() == "timeout" {
tempFile, errTemp := os.CreateTemp("", "algernon")
if errTemp != nil {
logrus.Fatal("Unable to find a temporary file to use:", errTemp)
}
ac.boltFilename = tempFile.Name() + ".db"
} else {
logrus.Errorf("Could not use Bolt as database backend: %s", err)
}
} else {
ac.dbName = "Bolt (" + ac.boltFilename + ")"
}
// Try the new database filename if there was a timeout
if ac.boltFilename != ac.defaultBoltFilename {
perm, err = bolt.NewWithConf(ac.boltFilename)
if err != nil {
if err.Error() == "timeout" {
logrus.Error("The Bolt database timed out!")
} else {
logrus.Errorf("Could not use Bolt as database backend: %s", err)
}
} else {
ac.dbName = "Bolt, temporary"
}
}
}
if ac.dbName == "" {
// This may typically happen if Algernon is already running
return nil, errors.New("could not find a usable database backend")
}
if ac.verboseMode {
logrus.Info("Database backend success: " + ac.dbName)
}
if perm != nil && ac.clearDefaultPathPrefixes {
perm.Clear()
}
return perm, nil
}