Ctrl K
ComunidadGuías…

engine/serverconf.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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
}