Ctrl K
ComunidadGuías…

engine/config.go

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

Archivo original ↗Preguntar a KiBot
// Package engine provides the server configuration struct and several functions for serving files over HTTP
package engine

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/rpc"
	"os"
	"path/filepath"
	"runtime"
	"slices"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/evanw/esbuild/pkg/api"
	"github.com/sirupsen/logrus"
	"github.com/xyproto/algernon/cachemode"
	"github.com/xyproto/algernon/lua/luastate"
	"github.com/xyproto/algernon/platformdep"
	"github.com/xyproto/algernon/utils"
	"github.com/xyproto/datablock"
	"github.com/xyproto/env/v2"
	"github.com/xyproto/mime"
	"github.com/xyproto/pinterface/v2"
	"github.com/xyproto/recwatch"
	"github.com/xyproto/unzip"
	"github.com/xyproto/vt"
)

const (
	// Version number. Stable API within major version numbers.
	Version = 2.0

	// The default supporting filename for a Lua script that provides data to a template
	defaultLuaDataFilename = "data.lua"
)

// Config is the main structure for the Algernon server.
// It contains all the state and settings.
// The order of the fields has been decided by the "fieldalignment" utility.
type Config struct {
	perm                         pinterface.IPermissions // the user state, for the permissions system
	mimereader                   *mime.Reader
	serverReadyFunctionLua       func()              // configuration that may only be set in the server configuration script(s)
	fs                           *datablock.FileStat // for checking if file exists, possibly in a cached way
	luapool                      *luastate.Pool      // a pool of Lua interpreters
	handlerPool                  *handlerPool        // a pool of Lua states for handle() requests
	cache                        *datablock.FileCache
	reverseProxyConfig           *ReverseProxyConfig
	bundleCache                  *bundleCache           // cache for on-the-fly esbuild bundles
	dirConfCache                 *dirConfigCache        // cache for parsed .algernon configurations
	pluginClients                map[string]*rpc.Client // cache of persistent plugin clients
	redisAddr                    string
	defaultEventPath             string
	defaultEventRefresh          string
	description                  string // description of the current program
	versionString                string // program name and version number
	defaultOpenExecutable        string // default program for opening files and URLs in the current operating system
	serverHost                   string
	defaultEventColonPort        string
	defaultLimitString           string // default rate limit, as a string
	limitRequestsString          string // store the request limit as a string for faster HTTP header creation later on
	defaultBoltFilename          string // default bolt database file, for some operating systems
	defaultLogFile               string // default log file, for some operating systems
	defaultRedisColonPort        string
	serverDirOrFilename          string // exposed to the server configuration scripts(s)
	serverAddr                   string // exposed to the server configuration scripts(s)
	serverConfScript             string // exposed to the server configuration scripts(s)
	defaultWebColonPort          string
	serverLogFile                string // exposed to the server configuration scripts(s)
	serverTempDir                string // temporary directory
	cookieSecret                 string // secret to be used when setting and getting user login cookies
	defaultTheme                 string // theme for Markdown and error pages
	openExecutable               string // open the URL after serving, with a specific executable
	serverAddrLua                string // configuration that may only be set in the server configuration script(s)
	dbName                       string
	serverHeaderName             string // used in the HTTP headers as the "Server" name
	eventAddr                    string // for the Server-Sent Event (SSE) server (host and port)
	eventRefresh                 string // for the Server-Sent Event (SSE) server (duration of an event cycle)
	luaServerFilename            string // if a single Lua file is provided, or if Server() is used
	autoRefreshDir               string // if only watching a single directory recursively
	combinedAccessLogFilename    string // CLF access log
	commonAccessLogFilename      string // NCSA access log
	combinedAccessLog            *logWriter
	commonAccessLog              *logWriter
	serverLog                    *logWriter // the --log file, if any
	boltFilename                 string
	internalLogFilename          string               // exposed to the server configuration scripts(s)
	mariadbDSN                   string               // connection string
	mariaDatabase                string               // database name
	sqliteConnectionString       string               // SQLite connection string
	postgresDSN                  string               // connection string
	postgresDatabase             string               // database name
	dirBaseURL                   string               // optional Base URL, for the directory listings
	jsxOptions                   api.TransformOptions // JSX rendering options
	serverConfigurationFilenames []string             // list of configuration filenames to check
	serve                        ServeConfig
	cacheMaxGivenDataSize        uint64
	largeFileSize                uint64        // threshold for not reading large files into memory
	bundleCacheMaxMemory         uint64        // max memory for bundle cache (0 = unlimited)
	refreshDuration              time.Duration // for the auto-refresh feature
	redisDBindex                 int
	cacheSize                    uint64
	cacheMode                    cachemode.Setting
	shutdownTimeout              time.Duration
	cacheMaxEntitySize           uint64
	defaultLimit                 int64
	defaultCacheMaxEntitySize    uint64        // 64 KiB
	defaultLargeFileSize         uint64        // 42 MiB: the default size for when a static file is large enough to not be read into memory
	limitRequests                int64         // rate limit to this many requests per client per second
	handlerPoolSize              int           // number of Lua states available for handle() request parallelism
	writeTimeout                 uint64        // timeout when writing data to a client, in seconds
	defaultStatCacheRefresh      time.Duration // refresh the stat cache, if the stat cache feature is enabled
	defaultCacheSize             uint64        // 1 MiB
	pluginClientsMu              sync.Mutex
	defaultPermissions           os.FileMode
	quietMode                    bool // no output to the command line
	autoRefresh                  bool // enable the event server and inject JavaScript to reload pages when sources change
	serverMode                   bool // server mode: non-interactive
	productionMode               bool // server mode: non-interactive, assume the server is running as a system service
	verboseMode                  bool // server mode: be more verbose
	debugMode                    bool // server mode: enable debug features and better error messages
	cacheFileStat                bool // assume files will not be removed from the served directories while Algernon is running, which allows caching of costly os.Stat calls
	serverAddDomain              bool // look for files in the directory with the same name as the requested hostname
	stricterHeaders              bool // stricter HTTP headers
	simpleMode                   bool // server mode: for serving a directory with files over regular HTTP, nothing more nothing less
	openURLAfterServing          bool // open the URL after serving
	onlyLuaMode                  bool // if only using the Lua REPL, and not serving anything
	quitAfterFirstRequest        bool // quit when the first request has been responded to?
	markdownMode                 bool
	serveJustQUIC                bool // if only QUIC or HTTP/3
	serveJustHTTP                bool // if only HTTP
	serveJustHTTP2               bool // if only HTTP/2
	ctrldTwice                   bool // require a double press of ctrl-d to exit the REPL
	noHeaders                    bool // HTTP headers
	redisAddrSpecified           bool
	noCache                      bool
	showVersion                  bool
	curlSupport                  bool // support clients like "curl" that downloads uncompressed by default
	noBanner                     bool // don't display the ANSI-graphics banner at start
	cacheCompressionSpeed        bool // compression speed over compactness
	cacheCompression             bool
	singleFileMode               bool // if only serving a single file, like a Lua script
	hyperApp                     bool // convert JSX to HyperApp JS, or React JS?
	devMode                      bool // server mode: aims to make it easy to get started
	clearDefaultPathPrefixes     bool // clear default path prefixes like "/admin" from the permission system?
	disableRateLimiting          bool
	useBolt                      bool
	useNoDatabase                bool // don't use a database. There will be a loss of functionality.
	separateEventServer          bool // use a dedicated port for the SSE event server
	hideDotfiles                 bool // hide files and directories starting with "."
	strictSymlinks               bool // don't follow symlinks that lead out of the served directory
	serverModeFromCLI            bool // --noninteractive / -s was set from the command line
}

// PortSetting describes a single listener endpoint with a protocol and TLS preference
type PortSetting struct {
	Addr     string // [host]:port
	Protocol string // "http", "http2", "http3" (or "quic"), "event"
	TLS      bool   // use TLS?
}

// ServeConfig groups all listener and TLS settings. It is the single source of
// truth for how and on which addresses/ports Algernon serves traffic.
type ServeConfig struct {
	httpAddr            string        // explicit HTTP listen address (from --http-addr or positional)
	httpsAddr           string        // explicit HTTPS listen address (from --https-addr or positional)
	serverCert          string        // exposed to the server configuration scripts(s)
	serverKey           string        // exposed to the server configuration scripts(s)
	portSettings        []PortSetting // explicit listener configuration (from SetPorts in Lua)
	certMagicDomains    []string
	redirectHTTP        bool // redirect HTTP traffic to HTTPS?
	useCertMagic        bool // use CertMagic and Let's Encrypt for all directories in the given directory that contains a "."
	useCertMagicStaging bool // use the Let's Encrypt staging CA instead of the production CA
	portConfigFromCLI   bool // port configuration was set from flags or positional args
	certMagicFromCLI    bool // --letsencrypt / -c was set from the command line
	redirectFromCLI     bool // --redirect was set from the command line
}

// ErrVersion is returned when the initialization quits because all that is done
// is showing version information
var (
	ErrVersion  = errors.New("only showing version information")
	ErrDatabase = errors.New("could not find a usable database backend")
)

// New creates a new server configuration based using the default values
func New(versionString, description string) (*Config, error) {
	tmpdir := env.Str("TMPDIR", "/tmp")
	ac := &Config{
		curlSupport: true,

		shutdownTimeout: 10 * time.Second,

		// Pool size for parallel Lua handle() requests. One state per CPU by default.
		handlerPoolSize: runtime.NumCPU(),

		defaultWebColonPort:       ":3000",
		defaultRedisColonPort:     ":6379",
		defaultEventColonPort:     ":5553",
		defaultEventRefresh:       "350ms",
		defaultEventPath:          "/sse",
		defaultLimit:              10,
		defaultPermissions:        0o660,
		defaultCacheSize:          1 * utils.MiB,   // 1 MiB
		defaultCacheMaxEntitySize: 64 * utils.KiB,  // 64 KB
		defaultStatCacheRefresh:   time.Minute * 1, // Refresh the stat cache, if the stat cache feature is enabled

		// Threshold for streaming static files instead of reading them
		// into memory. Also caps incoming request body size.
		defaultLargeFileSize: 42 * utils.MiB, // 42 MiB

		// Default rate limit, as a string
		defaultLimitString: strconv.Itoa(10),

		// Default Bolt database file, for some operating systems
		defaultBoltFilename: filepath.Join(tmpdir, "algernon.db"),

		// Default log file, for some operating systems
		defaultLogFile: filepath.Join(tmpdir, "algernon.log"),

		// List of configuration filenames to check
		serverConfigurationFilenames: []string{"/etc/algernon/serverconf.lua", "/etc/algernon/server.lua"},

		// Compression speed over compactness
		cacheCompressionSpeed: true,

		// TODO: Make configurable
		// Maximum given file size for caching, 7 MiB
		cacheMaxGivenDataSize: 7 * utils.MiB,

		// Program for opening URLs, keep empty for using the default in url.go
		defaultOpenExecutable: "",

		// General information about Algernon
		versionString: versionString,
		description:   description,

		// Cache for on-the-fly esbuild bundles
		bundleCache: newBundleCache(),

		// JSX rendering options
		jsxOptions: api.TransformOptions{
			Loader:            api.LoaderJSX,
			MinifyWhitespace:  true,
			MinifyIdentifiers: true,
			MinifySyntax:      true,
			Charset:           api.CharsetUTF8,
		},
	}
	if err := ac.initFilesAndCache(); err != nil {
		return nil, err
	}
	// Read in the mimetype information from the system. Set UTF-8 when setting Content-Type.
	ac.mimereader = mime.New("/etc/mime.types", true)
	ac.setupLogging()

	// File stat cache
	ac.fs = datablock.NewFileStat(ac.cacheFileStat, ac.defaultStatCacheRefresh)

	return ac, nil
}

// SetFileStatCache can be used to set a different FileStat cache than the default one
func (ac *Config) SetFileStatCache(fs *datablock.FileStat) {
	ac.fs = fs
}

// Initialize a temporary directory, handle flags, output version and handle profiling
func (ac *Config) initFilesAndCache() error {
	// Temporary directory that might be used for logging, databases or file extraction
	serverTempDir, err := os.MkdirTemp("", "algernon")
	if err != nil {
		return err
	}
	ac.serverTempDir = serverTempDir

	// Set several configuration variables, based on the given flags and arguments
	ac.handleFlags(ac.serverTempDir)

	// Version (--version)
	if ac.showVersion {
		if !ac.quietMode {
			fmt.Println(ac.versionString)
		}
		return ErrVersion
	}

	// CPU and memory profiling, if it is enabled at build time, and one of these
	// flags are provided (+ a filename): -cpuprofile, -memprofile, -fgtrace or -trace
	traceStart()

	// Open the common access log, if specified
	if ac.commonAccessLogFilename != "" {
		lw, err := openLogWriter(ac.commonAccessLogFilename, defaultLogPermissions)
		if err != nil {
			return err
		}
		ac.commonAccessLog = lw
	}
	// Open the combined access log, if specified
	if ac.combinedAccessLogFilename != "" {
		lw, err := openLogWriter(ac.combinedAccessLogFilename, defaultLogPermissions)
		if err != nil {
			return err
		}
		ac.combinedAccessLog = lw
	}
	// Create a cache struct for reading files (contains functions that can
	// be used for reading files, also when caching is disabled).
	// The final argument is for compressing with "fast" instead of "best".
	ac.cache = datablock.NewFileCache(ac.cacheSize, ac.cacheCompression, ac.cacheMaxEntitySize, ac.cacheCompressionSpeed, ac.cacheMaxGivenDataSize)
	return nil
}

func (ac *Config) setupLogging() {
	// Log to a file as JSON, if a log file has been specified
	if ac.serverLogFile != "" {
		lw, errJSONLog := openLogWriter(ac.serverLogFile, defaultLogPermissions)
		if errJSONLog != nil {
			logrus.Warnf("Could not log to %s: %s", ac.serverLogFile, errJSONLog)
		} else {
			// Log to the given log filename
			ac.serverLog = lw
			logrus.SetFormatter(&logrus.JSONFormatter{})
			logrus.SetOutput(lw)
		}
	} else if ac.quietMode {
		// If quiet mode is enabled and no log file has been specified, disable logging
		logrus.SetOutput(io.Discard)
	}
	// Close stdout and stderr if quite mode has been enabled
	if ac.quietMode {
		os.Stdout.Close()
		os.Stderr.Close()
	}
}

// Close removes the temporary directory
func (ac *Config) Close() {
	os.RemoveAll(ac.serverTempDir)
}

// Fatal exit
func (ac *Config) fatalExit(err error) {
	// Log to file, if a log file is used
	if ac.serverLogFile != "" {
		logrus.Error(err)
	}
	// Then switch to stderr and log the message there as well
	logrus.SetOutput(os.Stderr)
	// Use the standard formatter
	logrus.SetFormatter(&logrus.TextFormatter{})
	// Log and exit
	logrus.Fatalln(err.Error())
}

// Abrupt exit
func (ac *Config) abruptExit(msg string) {
	// Log to file, if a log file is used
	if ac.serverLogFile != "" {
		logrus.Info(msg)
	}
	// Then switch to stderr and log the message there as well
	logrus.SetOutput(os.Stderr)
	// Use the standard formatter
	logrus.SetFormatter(&logrus.TextFormatter{})
	// Log and exit
	logrus.Info(msg)
	os.Exit(0)
}

// Quit after a short duration
func (ac *Config) quitSoon(msg string, soon time.Duration) {
	time.Sleep(soon)
	ac.abruptExit(msg)
}

// Return true of the given file type (extension) should be cached
func (ac *Config) shouldCache(ext string) bool {
	switch ac.cacheMode {
	case cachemode.On:
		return true
	case cachemode.Production, cachemode.Small:
		switch ext {
		case ".amber", ".lua", ".po2", ".pongo2", ".tl", ".tpl":
			return false
		default:
			return true
		}
	case cachemode.Images:
		switch ext {
		case ".apng", ".bmp", ".gif", ".ico", ".jpeg", ".jpg", ".png", ".qoi", ".svg", ".webp":
			return true
		default:
			return false
		}
	case cachemode.Off:
		return false
	case cachemode.Development, cachemode.Unset:
		fallthrough
	default:
		switch ext {
		case ".amber", ".gcss", ".happ", ".js", ".jsx", ".lua", ".md", ".po2", ".pongo2", ".scss", ".ts", ".tsx", ".tl", ".tpl":
			return false
		default:
			return true
		}
	}
}

// hasHandlers checks if the given filename contains "handle(" or "handle ("
func hasHandlers(fn string) bool {
	data, err := os.ReadFile(fn)
	return err == nil && (bytes.Contains(data, []byte("handle(")) || bytes.Contains(data, []byte("handle (")))
}

// has checks if a given slice of strings contains a given string
func has(sl []string, e string) bool {
	return slices.Contains(sl, e)
}

// unique removes all repeated elements from a slice of strings
func unique(sl []string) []string {
	var nl []string
	for _, s := range sl {
		if !has(nl, s) {
			nl = append(nl, s)
		}
	}
	return nl
}

// globalsLuaPath returns the expected location of an optional globals.lua next
// to the served root, or "" if a path cannot be derived.
func globalsLuaPath(serverDirOrFilename string, singleFileMode bool) string {
	if serverDirOrFilename == "" {
		return ""
	}
	if singleFileMode {
		return filepath.Join(filepath.Dir(serverDirOrFilename), "globals.lua")
	}
	return filepath.Join(serverDirOrFilename, "globals.lua")
}

// MustServe sets up a server with handlers
func (ac *Config) MustServe(mux *http.ServeMux) error {
	var err error

	defer ac.Close()

	// Output what we are attempting to access and serve
	if ac.verboseMode {
		logrus.Info("Accessing " + ac.serverDirOrFilename)
	}

	// Check if the given directory really is a directory
	if !ac.fs.IsDir(ac.serverDirOrFilename) {
		// It is not a directory
		serverFile := ac.serverDirOrFilename

		// Return with an error if the file to serve does not exist
		if !ac.fs.Exists(serverFile) {
			return fmt.Errorf("file does not exist: %s", serverFile)
		}

		if ac.markdownMode {
			// Serve the given Markdown file as a static HTTP server
			if serveErr := ac.ServeStaticFile(serverFile, ac.defaultWebColonPort); serveErr != nil {
				// Must serve
				ac.fatalExit(serveErr)
			}
			return nil
		}
		// Switch based on the lowercase filename extension
		switch strings.ToLower(filepath.Ext(serverFile)) {
		case ".md", ".markdown":
			// Serve the given Markdown file as a static HTTP server
			if serveErr := ac.ServeStaticFile(serverFile, ac.defaultWebColonPort); serveErr != nil {
				// Must serve
				ac.fatalExit(serveErr)
			}
			return nil
		case ".zip", ".alg":

			// Assume this to be a compressed Algernon application
			webApplicationExtractionDir := "/dev/shm" // extract to memory, if possible
			testfile := filepath.Join(webApplicationExtractionDir, "canary")
			if _, err := os.Create(testfile); err == nil { // success
				os.Remove(testfile)
			} else {
				// Could not create the test file
				// Use the server temp dir (typically /tmp) instead of /dev/shm
				webApplicationExtractionDir = ac.serverTempDir
			}
			// Extract the web application
			if extractErr := unzip.Extract(serverFile, webApplicationExtractionDir); extractErr != nil {
				return extractErr
			}
			// Use the directory where the file was extracted as the server directory
			ac.serverDirOrFilename = webApplicationExtractionDir
			// If there is only one directory there, assume it's the
			// directory of the newly extracted ZIP file.
			if filenames := utils.GetFilenames(ac.serverDirOrFilename); len(filenames) == 1 {
				fullPath := filepath.Join(ac.serverDirOrFilename, filenames[0])
				if ac.fs.IsDir(fullPath) {
					// Use this as the server directory instead
					ac.serverDirOrFilename = fullPath
				}
			}
			// If there are server configuration files in the extracted
			// directory, register them.
			for _, filename := range ac.serverConfigurationFilenames {
				configFilename := filepath.Join(ac.serverDirOrFilename, filename)
				ac.serverConfigurationFilenames = append(ac.serverConfigurationFilenames, configFilename)
			}
			// Disregard all configuration files from the current directory
			// (filenames without a path separator), since we are serving a
			// ZIP file.
			for i, filename := range ac.serverConfigurationFilenames {
				if strings.Count(filepath.ToSlash(filename), "/") == 0 {
					// Remove the filename from the slice
					ac.serverConfigurationFilenames = append(ac.serverConfigurationFilenames[:i], ac.serverConfigurationFilenames[i+1:]...)
				}
			}

		default:
			ac.singleFileMode = true
		}

	}

	// Make a few changes to the defaults if we are serving a single file
	if ac.singleFileMode {
		ac.serveJustHTTP = true
	}

	to := vt.NewTextOutput(!platformdep.IsWindows, !ac.quietMode)

	// Console output
	if !ac.quietMode && !ac.singleFileMode && !ac.simpleMode && !ac.noBanner {
		// Output a colorful ansi logo if a proper terminal is available
		fmt.Println(Banner(ac.versionString, ac.description))
	} else if !ac.quietMode && !ac.singleFileMode {
		timestamp := time.Now().Format("2006-01-02 15:04")
		to.OutputTags("<cyan>" + ac.versionString + "<darkgray> - " + timestamp + "<off>")
	}

	// Disable the database backend if the BoltDB filename is the /dev/null file (or OS equivalent)
	if ac.boltFilename == os.DevNull {
		ac.useNoDatabase = true
	}

	if !ac.useNoDatabase {
		// Connect to a database and retrieve a Permissions struct
		ac.perm, err = ac.DatabaseBackend()
		if err != nil {
			return ErrDatabase
		}
	}

	// Lua LState pool
	ac.luapool = luastate.New()
	AtShutdown(func() {
		ac.luapool.Shutdown()
	})

	// Auto-detect globals.lua next to the served root and apply it to every
	// new pool state. See issue #103.
	globalsPath := globalsLuaPath(ac.serverDirOrFilename, ac.singleFileMode)
	if globalsPath != "" {
		if data, err := os.ReadFile(globalsPath); err == nil {
			ac.luapool.SetGlobalsScript(data)
			if ac.verboseMode {
				logrus.Info("Loaded globals.lua from ", globalsPath)
			}
		} else if !os.IsNotExist(err) {
			logrus.Errorf("globals.lua: %s", err)
		}
	}
	AtShutdown(ac.closePluginClients)

	// TODO: save repl history + close luapool + close logs ++ at shutdown

	if ac.singleFileMode && (filepath.Ext(ac.serverDirOrFilename) == ".lua" || ac.onlyLuaMode) {
		ac.luaServerFilename = ac.serverDirOrFilename
		if ac.luaServerFilename == "index.lua" || ac.luaServerFilename == "data.lua" {
			// Friendly message to new users
			if !hasHandlers(ac.luaServerFilename) {
				logrus.Warnf("Found no handlers in %s", ac.luaServerFilename)
				logrus.Info("How to implement \"Hello, World!\" in " + ac.luaServerFilename + " file:\n\nhandle(\"/\", function()\n  print(\"Hello, World!\")\nend)\n")
			}
		}
		ac.serverDirOrFilename = filepath.Dir(ac.serverDirOrFilename)
		// Make it possible to read other files from the Lua script
		ac.singleFileMode = false
	}

	ac.serverConfigurationFilenames = unique(ac.serverConfigurationFilenames)

	// Color scheme
	arrowColor := "<lightblue>"
	filenameColor := "<white>"
	luaOutputColor := "<darkgray>"
	dashLineColor := "<red>"

	// Create a Colorize struct that will not reset colors after colorizing
	// strings meant for the terminal.
	// c := colorstring.Colorize{Colors: colorstring.DefaultColors, Reset: false}

	if (len(ac.serverConfigurationFilenames) > 0) && !ac.quietMode && !ac.onlyLuaMode {
		fmt.Println(to.Tags(dashLineColor + strings.Repeat("-", 49) + "<off>"))
	}

	// Read server configuration script, if present.
	// The scripts may change global variables.
	var ranConfigurationFilenames []string
	for _, filename := range unique(ac.serverConfigurationFilenames) {
		if ac.fs.Exists(filename) {
			// Dividing line between the banner and output from any of the configuration scripts
			if !ac.quietMode && !ac.onlyLuaMode {
				// Output the configuration filename
				to.Println(arrowColor + "-> " + filenameColor + filename + "<off>")
				fmt.Print(to.Tags(luaOutputColor))
			} else if ac.verboseMode {
				logrus.Info("Running Lua configuration file: " + filename)
			}
			withHandlerFunctions := true
			errConf := ac.RunConfiguration(filename, mux, withHandlerFunctions)
			if errConf != nil {
				if ac.perm != nil {
					logrus.Error("Could not use configuration script: " + filename)
					return errConf
				}
				if ac.verboseMode {
					logrus.Info("Skipping " + filename + " because the database backend is not in use.")
				}
			}
			ranConfigurationFilenames = append(ranConfigurationFilenames, filename)
		} else {
			if ac.verboseMode {
				logrus.Info("Looking for: " + filename)
			}
		}
	}
	// Only keep the active ones. Used when outputting server information.
	ac.serverConfigurationFilenames = ranConfigurationFilenames

	// Run the standalone Lua server, if specified
	if ac.luaServerFilename != "" {
		// Run the Lua server file and set up handlers
		if !ac.quietMode && !ac.onlyLuaMode {
			// Output the configuration filename
			to.Println(arrowColor + "-> " + filenameColor + ac.luaServerFilename + "<off>")
			fmt.Print(to.Tags(luaOutputColor))
		} else if ac.verboseMode {
			fmt.Println("Running Lua configuration file: " + ac.luaServerFilename)
		}
		withHandlerFunctions := true
		errLua := ac.RunConfiguration(ac.luaServerFilename, mux, withHandlerFunctions)
		if errLua != nil {
			return fmt.Errorf("error in %s (interpreted as a server script):\n%w", ac.luaServerFilename, errLua)
		}
	} else {
		// Register HTTP handler functions
		ac.RegisterHandlers(mux, "/", ac.serverDirOrFilename, ac.serverAddDomain)
	}

	// Register the React 19 endpoints
	registerReact19Handlers(mux)

	// Register the HMR endpoints when auto-refresh is active
	if ac.autoRefresh {
		mux.HandleFunc(hmrUpdatePrefix, ac.HMRUpdateHandler)
		mux.HandleFunc(hmrRefreshRuntimePath, ac.HMRRefreshRuntimeHandler)
	}

	// If --eventserver was explicitly provided, use a separate SSE server
	ac.separateEventServer = ac.eventAddr != ""

	// Set the values that has not been set by flags nor scripts
	// (and can be set by both)
	ranServerReadyFunction := ac.finalConfiguration(ac.serverHost)

	if !ac.quietMode && !ac.onlyLuaMode {
		to.Print("<off>")
	}

	// If no configuration files were being ran successfully,
	// output basic server information.
	if len(ac.serverConfigurationFilenames) == 0 {
		if !ac.quietMode && !ac.onlyLuaMode {
			fmt.Println(ac.Info())
		}
		ranServerReadyFunction = true
	}

	// Separator between the output of the configuration scripts and
	// the rest of the server output.
	if ranServerReadyFunction && (len(ac.serverConfigurationFilenames) > 0) && !ac.quietMode && !ac.onlyLuaMode {
		to.Tags(dashLineColor + strings.Repeat("-", 49) + "<off>")
	}

	// Direct internal logging elsewhere. Open for writing so that a newly
	// specified filename is created rather than falling through to the
	// hard-coded "internal.log" fallback.
	internalLogFile, err := os.OpenFile(ac.internalLogFilename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, ac.defaultPermissions)
	if err != nil {
		// Could not open the internalLogFilename filename, try using another filename
		internalLogFile, err = os.OpenFile("internal.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, ac.defaultPermissions)
		AtShutdown(func() {
			// TODO This one is is special and should be closed after the other shutdown functions.
			//      Set up a "done" channel instead of sleeping.
			time.Sleep(100 * time.Millisecond)
			internalLogFile.Close()
		})
		if err != nil {
			ac.fatalExit(fmt.Errorf("could not write to %s nor %s", ac.internalLogFilename, "internal.log"))
		}
	}
	defer internalLogFile.Close()
	log.SetOutput(internalLogFile)

	// Serve filesystem events in the background.
	// Used for reloading pages when the sources change.
	// Can also be used when serving a single file.
	if ac.autoRefresh {
		ac.refreshDuration, err = time.ParseDuration(ac.eventRefresh)
		if err != nil {
			logrus.Warnf("%s is an invalid duration. Using %s instead.", ac.eventRefresh, ac.defaultEventRefresh)
			// Ignore the error, since defaultEventRefresh is a constant and must be parseable
			ac.refreshDuration, _ = time.ParseDuration(ac.defaultEventRefresh)
		}
		recwatch.SetVerbose(ac.verboseMode)
		recwatch.LogError = func(err error) {
			logrus.Error(err)
		}
		recwatch.FatalExit = ac.fatalExit
		recwatch.Exists = ac.fs.Exists

		watchDir := ac.serverDirOrFilename
		if ac.autoRefreshDir != "" {
			watchDir = ac.autoRefreshDir
		}
		absdir, err := filepath.Abs(watchDir)
		if err != nil {
			absdir = watchDir
		}

		// Mount the SSE handler on the main mux by default (same port, no
		// CORS needed). If --eventserver was explicitly provided, start a
		// dedicated SSE server on that address instead.
		if ac.separateEventServer {
			sseScheme := "https"
			if ac.serveJustHTTP {
				sseScheme = "http"
			}
			sseAllowed := sseScheme + "://" + ac.serverAddr
			recwatch.EventServer(absdir, sseAllowed, ac.eventAddr, ac.defaultEventPath, ac.refreshDuration)
		} else {
			sseHandler, err := recwatch.EventServerHandler(absdir, "", ac.defaultEventPath, ac.refreshDuration)
			if err != nil {
				logrus.Error("Could not set up SSE file watcher: ", err)
			} else {
				mux.Handle(ac.defaultEventPath, sseHandler)
			}
		}
	}

	// For communicating to and from the REPL
	ready := make(chan bool) // for when the server is up and running
	done := make(chan bool)  // for when the user wish to quit the server

	// The Lua REPL
	if !ac.serverMode {
		// If the REPL uses readline, the SIGWINCH signal is handled there
		go ac.REPL(ready, done)
	} else {
		// Ignore SIGWINCH if we are not going to use a REPL
		platformdep.IgnoreTerminalResizeSignal()
	}

	// Setup a signal handler for clearing the cache when USR1 is received, for some platforms
	platformdep.SetupSignals(ac.ClearCache, logrus.Infof)

	// Re-open the log files when HUP is received, for log rotation
	platformdep.SetupLogRotationSignal(ac.ReopenLogs, logrus.Infof)

	// Run the shutdown functions if graceful does not
	defer ac.GenerateShutdownFunction(nil)()

	// Serve HTTP, HTTP/2 and/or HTTPS
	return ac.Serve(mux, done, ready)
}