package engine import ( "flag" "math" "net" "os" "path/filepath" "strconv" "strings" "github.com/sirupsen/logrus" "github.com/xyproto/algernon/cachemode" "github.com/xyproto/algernon/platformdep" "github.com/xyproto/algernon/themes" "github.com/xyproto/algernon/utils" "github.com/xyproto/datablock" "github.com/xyproto/env/v2" "github.com/xyproto/files" ) // positionalArgs is the result of classifying Algernon's free-form positional arguments type positionalArgs struct { // ServerDirOrFilename is an existing directory or file given on the command line ServerDirOrFilename string // ServerAddr is set from a "host:port", ":port" or bare web-port argument ServerAddr string // ServerAddr2 is set when a second address-like argument is found ServerAddr2 string // RedisAddr is set when an argument looks like a Redis address (":6379" or ":6380") RedisAddr string // Remaining holds unclassified arguments, in the legacy order: cert, key, redis address, redis db index Remaining []string ServerDirSet bool RedisAddrFromArgs bool } // Parse the flags, return the default hostname func (ac *Config) handleFlags(serverTempDir string) { var ( // The short version of some flags serveJustHTTPShort, autoRefreshShort, productionModeShort, debugModeShort, serverModeShort, useBoltShort, devModeShort, showVersionShort, quietModeShort, cacheFileStatShort, simpleModeShort, noBannerShort, quitAfterFirstRequestShort, verboseModeShort, serveJustQUICShort, onlyLuaModeShort, redirectShort, serverAddDomainShort, nonInteractive bool // Used when setting the cache mode cacheModeString string // Used if disabling cache compression rawCache bool // Used if disabling the database backend noDatabase bool ) // The default for running the redis server on Windows is to listen // to "localhost:port", but not just ":port". host := "" if platformdep.IsWindows { host = "localhost" // Default Bolt database file ac.defaultBoltFilename = filepath.Join(serverTempDir, "algernon.db") // Default log file ac.defaultLogFile = filepath.Join(serverTempDir, "algernon.log") } // The usage function that provides more help (for --help or -h) flag.Usage = generateUsageFunction(ac) // Commandline flag configuration flag.StringVar(&ac.serverDirOrFilename, "dir", ".", "Server directory") flag.StringVar(&ac.serverAddr, "addr", "", "Server [host][:port] (ie \":443\" or \"[::1]:443\")") flag.StringVar(&ac.serve.httpAddr, "http-addr", "", "HTTP (non-TLS) [host][:port]") flag.StringVar(&ac.serve.httpsAddr, "https-addr", "", "HTTPS (TLS) [host][:port]") flag.StringVar(&ac.serve.serverCert, "cert", "cert.pem", "Server certificate") flag.StringVar(&ac.serve.serverKey, "key", "key.pem", "Server key") flag.StringVar(&ac.redisAddr, "redis", "", "Redis [host][:port] (ie \""+ac.defaultRedisColonPort+"\")") flag.IntVar(&ac.redisDBindex, "dbindex", 0, "Redis database index") flag.StringVar(&ac.serverConfScript, "conf", "serverconf.lua", "Server configuration written in Lua") flag.StringVar(&ac.serverLogFile, "log", "", "Server log file") flag.StringVar(&ac.internalLogFilename, "internal", os.DevNull, "Internal log file") flag.BoolVar(&ac.serveJustHTTP2, "http2only", false, "Serve HTTP/2, not HTTPS + HTTP/2") flag.BoolVar(&ac.serveJustHTTP, "httponly", false, "Serve plain old HTTP") flag.BoolVar(&ac.productionMode, "prod", false, "Production mode (when running as a system service)") flag.BoolVar(&ac.debugMode, "debug", false, "Debug mode") flag.BoolVar(&ac.verboseMode, "verbose", false, "Verbose logging") flag.BoolVar(&ac.serve.redirectHTTP, "redirect", false, "Redirect HTTP traffic to HTTPS if both are enabled") flag.BoolVar(&ac.autoRefresh, "autorefresh", false, "Enable the auto-refresh feature") flag.StringVar(&ac.autoRefreshDir, "watchdir", "", "Directory to watch (also enables auto-refresh)") flag.StringVar(&ac.eventAddr, "eventserver", "", "SSE [host][:port] (ie \""+ac.defaultEventColonPort+"\")") flag.StringVar(&ac.eventRefresh, "eventrefresh", ac.defaultEventRefresh, "Event refresh interval (ie \""+ac.defaultEventRefresh+"\")") flag.BoolVar(&ac.serverMode, "server", false, "Same as --noninteractive") // deprecated, use --noninteractive or -s flag.BoolVar(&nonInteractive, "noninteractive", false, "Non-interactive mode (disable debug + interactive mode)") flag.StringVar(&ac.mariadbDSN, "maria", "", "MariaDB/MySQL connection string (DSN)") flag.StringVar(&ac.mariaDatabase, "mariadb", "", "MariaDB/MySQL database name") flag.StringVar(&ac.postgresDSN, "postgres", "", "PostgreSQL connection string (DSN)") flag.StringVar(&ac.postgresDatabase, "postgresdb", "", "PostgreSQL database name") flag.StringVar(&ac.sqliteConnectionString, "sqlite", "", "SQLite filename / connection string") flag.BoolVar(&ac.useBolt, "bolt", false, "Use the default Bolt filename") flag.StringVar(&ac.boltFilename, "boltdb", "", "Bolt database filename") flag.Int64Var(&ac.limitRequests, "limit", ac.defaultLimit, "Limit clients to a number of requests per second") flag.BoolVar(&ac.disableRateLimiting, "nolimit", false, "Disable rate limiting") flag.BoolVar(&ac.devMode, "dev", false, "Development mode") flag.BoolVar(&ac.showVersion, "version", false, "Version") flag.StringVar(&cacheModeString, "cache", "", "Cache everything but Amber, Lua, GCSS and Markdown") flag.Uint64Var(&ac.cacheSize, "cachesize", ac.defaultCacheSize, "Cache size, in bytes") flag.Uint64Var(&ac.largeFileSize, "largesize", ac.defaultLargeFileSize, "Threshold for not reading static files into memory, in bytes") flag.Uint64Var(&ac.writeTimeout, "timeout", 10, "Timeout when writing to a client, in seconds") flag.BoolVar(&ac.quietMode, "quiet", false, "Quiet") flag.BoolVar(&rawCache, "rawcache", false, "Disable cache compression") flag.StringVar(&ac.serverHeaderName, "servername", ac.versionString, "Server header name") flag.BoolVar(&ac.cacheFileStat, "statcache", false, "Cache os.Stat") flag.BoolVar(&ac.serverAddDomain, "domain", false, "Look for files in the directory named the same as the hostname") flag.BoolVar(&ac.simpleMode, "simple", false, "Serve a directory of files over HTTP") flag.StringVar(&ac.openExecutable, "open", "", "Open URL after serving, with an application") flag.BoolVar(&ac.quitAfterFirstRequest, "quit", false, "Quit after the first request") flag.BoolVar(&ac.noCache, "nocache", false, "Disable caching") flag.BoolVar(&ac.noHeaders, "noheaders", false, "Don't set any HTTP headers by default") flag.BoolVar(&ac.stricterHeaders, "stricter", false, "Stricter HTTP headers") flag.StringVar(&ac.defaultTheme, "theme", themes.DefaultTheme, "Theme for Markdown and directory listings") flag.BoolVar(&ac.noBanner, "nobanner", false, "Don't show a banner at start") flag.BoolVar(&ac.ctrldTwice, "ctrld", false, "Press ctrl-d twice to exit") if quicEnabled { flag.BoolVar(&ac.serveJustQUIC, "quic", false, "Serve just QUIC") } flag.BoolVar(&noDatabase, "nodb", false, "No database backend") flag.BoolVar(&ac.onlyLuaMode, "lua", false, "Only present the Lua REPL") flag.StringVar(&ac.combinedAccessLogFilename, "accesslog", "", "Combined access log filename") flag.StringVar(&ac.commonAccessLogFilename, "ncsa", "", "NCSA access log filename") flag.BoolVar(&ac.clearDefaultPathPrefixes, "clear", false, "Clear the default URI prefixes for handling permissions") flag.StringVar(&ac.cookieSecret, "cookiesecret", "", "Secret to be used when setting and getting login cookies") flag.BoolVar(&ac.serve.useCertMagic, "letsencrypt", false, "Use Let's Encrypt for all served domains and serve regular HTTPS") flag.BoolVar(&ac.serve.useCertMagicStaging, "testcert", false, "Use the Let's Encrypt staging CA instead of the production CA (for testing)") flag.BoolVar(&ac.hideDotfiles, "hide-dotfiles", false, "Hide files and directories starting with '.'") flag.BoolVar(&ac.strictSymlinks, "strict-symlinks", false, "Don't follow symlinks that lead out of the served directory") flag.StringVar(&ac.dirBaseURL, "dirbaseurl", "", "Base URL for the directory listing (optional)") flag.Func("proxy", "Reverse proxy to ENDPOINT, or /PREFIX=ENDPOINT (can be given several times)", func(value string) error { rp, err := ParseReverseProxyFlag(value) if err != nil { return err } if ac.reverseProxyConfig == nil { ac.reverseProxyConfig = NewReverseProxyConfig() } ac.reverseProxyConfig.Add(rp) return nil }) // The short versions of some flags flag.BoolVar(&serveJustHTTPShort, "t", false, "Serve plain old HTTP") flag.BoolVar(&autoRefreshShort, "a", false, "Enable the auto-refresh feature") flag.BoolVar(&serverModeShort, "s", false, "Non-interactive mode (short for --noninteractive)") flag.BoolVar(&useBoltShort, "b", false, "Use the default Bolt filename") flag.BoolVar(&productionModeShort, "p", false, "Production mode (when running as a system service)") flag.BoolVar(&debugModeShort, "d", false, "Debug mode") flag.BoolVar(&devModeShort, "e", false, "Development mode") flag.BoolVar(&showVersionShort, "v", false, "Version") flag.BoolVar(&verboseModeShort, "V", false, "Verbose") flag.BoolVar(&quietModeShort, "q", false, "Quiet") flag.BoolVar(&cacheFileStatShort, "c", false, "Cache os.Stat") flag.BoolVar(&simpleModeShort, "x", false, "Simple mode") flag.BoolVar(&ac.openURLAfterServing, "o", false, "Open URL after serving") flag.BoolVar(&quitAfterFirstRequestShort, "z", false, "Quit after the first request") flag.BoolVar(&ac.markdownMode, "m", false, "Markdown mode") flag.BoolVar(&noBannerShort, "n", false, "Don't show a banner at start") if quicEnabled { flag.BoolVar(&serveJustQUICShort, "u", false, "Serve just QUIC") } flag.BoolVar(&onlyLuaModeShort, "l", false, "Only present the Lua REPL") flag.BoolVar(&redirectShort, "r", false, "Redirect HTTP traffic to HTTPS, if both are enabled") flag.BoolVar(&serverAddDomainShort, "D", false, "Look for files in the directory named the same as the hostname") flag.Parse() // Accept both long and short versions of some flags ac.serveJustHTTP = ac.serveJustHTTP || serveJustHTTPShort ac.autoRefresh = ac.autoRefresh || autoRefreshShort ac.debugMode = ac.debugMode || debugModeShort ac.serverMode = ac.serverMode || serverModeShort ac.serverAddDomain = ac.serverAddDomain || serverAddDomainShort // --noninteractive / -s is the preferred way to enable server mode if nonInteractive || serverModeShort { ac.serverMode = true } // Track which settings were explicitly set from the command line if ac.serverMode { ac.serverModeFromCLI = true } if ac.serve.redirectHTTP { ac.serve.redirectFromCLI = true } if ac.serve.useCertMagic { ac.serve.certMagicFromCLI = true } ac.useBolt = ac.useBolt || useBoltShort ac.productionMode = ac.productionMode || productionModeShort ac.devMode = ac.devMode || devModeShort ac.showVersion = ac.showVersion || showVersionShort ac.quietMode = ac.quietMode || quietModeShort ac.cacheFileStat = ac.cacheFileStat || cacheFileStatShort ac.simpleMode = ac.simpleMode || simpleModeShort ac.openURLAfterServing = ac.openURLAfterServing || (ac.openExecutable != "") ac.quitAfterFirstRequest = ac.quitAfterFirstRequest || quitAfterFirstRequestShort ac.verboseMode = ac.verboseMode || verboseModeShort ac.noBanner = ac.noBanner || noBannerShort if quicEnabled { ac.serveJustQUIC = ac.serveJustQUIC || serveJustQUICShort } ac.onlyLuaMode = ac.onlyLuaMode || onlyLuaModeShort ac.serve.redirectHTTP = ac.serve.redirectHTTP || redirectShort // --largesize is passed to http.MaxBytesReader as int64; reject // values that would wrap to negative rather than silently truncate. if ac.largeFileSize > math.MaxInt64 { logrus.Fatalf("--largesize is too large: %d exceeds the maximum supported value of %d bytes", ac.largeFileSize, int64(math.MaxInt64)) } // Serve a single Markdown file once, and open it in the browser if ac.markdownMode { ac.quietMode = true ac.openURLAfterServing = true ac.quitAfterFirstRequest = true } // If only using the Lua REPL, don't include the banner, and don't serve anything if ac.onlyLuaMode { ac.noBanner = true ac.debugMode = true ac.serverConfScript = "" } // Check if IGNOREEOF is set if ignoreEOF := env.Int("IGNOREEOF", 0); ignoreEOF > 1 { ac.ctrldTwice = true } // Disable verbose mode if quiet mode has been enabled if ac.quietMode { ac.verboseMode = false } // Enable cache compression unless raw cache is specified ac.cacheCompression = !rawCache ac.redisAddrSpecified = ac.redisAddr != "" if ac.redisAddr == "" { // The default host and port ac.redisAddr = utils.JoinHostPort(host, ac.defaultRedisColonPort) } // May be overridden by devMode if ac.serverMode { ac.debugMode = false } if noDatabase { ac.boltFilename = os.DevNull } // TODO: If flags are set in addition to -p or -e, don't override those // when -p or -e is set. // Change several defaults if production mode is enabled switch { case ac.productionMode: // Use system directories ac.serverDirOrFilename = "/srv/algernon" ac.serve.serverCert = "/etc/algernon/cert.pem" ac.serve.serverKey = "/etc/algernon/key.pem" ac.cacheMode = cachemode.Production ac.serverMode = true case ac.devMode: // Change several defaults if development mode is enabled ac.serveJustHTTP = true // serverLogFile = defaultLogFile ac.debugMode = true // TODO: Make it possible to set --limit to the default limit also when -e is used if ac.limitRequests == ac.defaultLimit { ac.limitRequests = 700 // Increase the rate limit considerably } ac.cacheMode = cachemode.Development case ac.simpleMode: ac.useBolt = true ac.boltFilename = os.DevNull ac.serveJustHTTP = true ac.serverMode = true ac.cacheMode = cachemode.Off ac.noCache = true ac.disableRateLimiting = true ac.clearDefaultPathPrefixes = true ac.noHeaders = true ac.writeTimeout = 3600 * 24 } if ac.onlyLuaMode { // Use a random database, so that several lua REPLs can be started without colliding, // but only if the current default bolt database file can not be opened. if ac.boltFilename != os.DevNull && !files.CanRead(ac.boltFilename) { tempFile, err := os.CreateTemp("", "algernon_repl*.db") if err == nil { // no issue ac.boltFilename = tempFile.Name() } } } // If a watch directory is given, enable the auto refresh feature if ac.autoRefreshDir != "" { ac.autoRefresh = true } // If nocache is given, disable the cache if ac.noCache { ac.cacheMode = cachemode.Off ac.cacheFileStat = false } // Convert the request limit to a string ac.limitRequestsString = strconv.FormatInt(ac.limitRequests, 10) // If auto-refresh is enabled, change the caching and disable minification if ac.autoRefresh { if cacheModeString == "" { // Disable caching by default, when auto-refresh is enabled ac.cacheMode = cachemode.Off ac.cacheFileStat = false } // Readable output is more useful than minified output during development ac.jsxOptions.MinifyWhitespace = false ac.jsxOptions.MinifyIdentifiers = false ac.jsxOptions.MinifySyntax = false } // The cache flag overrides the settings from the other modes if cacheModeString != "" { ac.cacheMode = cachemode.New(cacheModeString) } // Disable cache entirely if cacheSize is set to 0 if ac.cacheSize == 0 { ac.cacheMode = cachemode.Off } // Set cacheSize to 0 if the cache is disabled if ac.cacheMode == cachemode.Off { ac.cacheSize = 0 } // If cache mode is unset, use the dev mode if ac.cacheMode == cachemode.Unset { ac.cacheMode = cachemode.Default } if ac.cacheMode == cachemode.Small { ac.cacheMaxEntitySize = ac.defaultCacheMaxEntitySize } // For backward compatibility with previous versions of Algernon. // Classify positional arguments by content rather than by position, // so that the directory and address can be given in any order. fs := datablock.NewFileStat(ac.cacheFileStat, ac.defaultStatCacheRefresh) classified := classifyPositionalArgs(dropSelfArg(flag.Args()), func(p string) bool { return fs.IsDir(p) || fs.Exists(p) }) if classified.ServerDirSet { ac.serverDirOrFilename = classified.ServerDirOrFilename } if classified.ServerAddr != "" { ac.serverAddr = classified.ServerAddr } // Two positional addresses: first = HTTP, second = HTTPS (only if flags didn't set them) if classified.ServerAddr2 != "" { if ac.serve.httpAddr == "" { ac.serve.httpAddr = classified.ServerAddr } if ac.serve.httpsAddr == "" { ac.serve.httpsAddr = classified.ServerAddr2 } ac.serverAddr = "" // will be derived from httpAddr/httpsAddr in serve.go } // Track that port configuration came from the command line. This includes // a single positional "host:port" (classified into ac.serverAddr), so that // flag-driven modes like -p do not silently override the user's choice. if ac.serve.httpAddr != "" || ac.serve.httpsAddr != "" || ac.serverAddr != "" { ac.serve.portConfigFromCLI = true } if classified.RedisAddrFromArgs { ac.redisAddr = classified.RedisAddr ac.redisAddrSpecified = true } remainingArgs := classified.Remaining redisAddrFromArgs := classified.RedisAddrFromArgs // Clean up path in ac.serverDirOrFilename // .Rel calls .Clean on the result. if pwd, err := os.Getwd(); err == nil { // no error if cleanPath, err := filepath.Rel(pwd, ac.serverDirOrFilename); err == nil { // no error ac.serverDirOrFilename = cleanPath } } // Handle remaining unclassified positional arguments for backward compatibility. // Expected order: cert file, key file, redis address, redis db index. if len(remainingArgs) > 0 { ac.serve.serverCert = remainingArgs[0] } if len(remainingArgs) > 1 { ac.serve.serverKey = remainingArgs[1] } if len(remainingArgs) > 2 && !redisAddrFromArgs { ac.redisAddr = remainingArgs[2] ac.redisAddrSpecified = true } dbIndexArgIndex := 3 if redisAddrFromArgs { dbIndexArgIndex = 2 } if len(remainingArgs) > dbIndexArgIndex { // Convert the dbindex from string to int DBindex, err := strconv.Atoi(remainingArgs[dbIndexArgIndex]) if err == nil { // no error ac.redisDBindex = DBindex } } // Add the serverConfScript to the list of configuration scripts to be read and executed if ac.serverConfScript != "" && ac.serverConfScript != os.DevNull { ac.serverConfigurationFilenames = append(ac.serverConfigurationFilenames, ac.serverConfScript, filepath.Join(ac.serverDirOrFilename, ac.serverConfScript)) } ac.serverHost = host // CertMagic and Let's Encrypt if ac.serve.useCertMagic { logrus.Info("Use Cert Magic") if dirEntries, err := os.ReadDir(ac.serverDirOrFilename); err != nil { logrus.Error("Could not use Cert Magic:" + err.Error()) ac.serve.useCertMagic = false } else { for _, dirEntry := range dirEntries { basename := filepath.Base(dirEntry.Name()) dirOrSymlink := dirEntry.IsDir() || ((dirEntry.Type() & os.ModeSymlink) == os.ModeSymlink) // TODO: Confirm that the symlink is a symlink to a directory, if it's a symlink if dirOrSymlink && strings.Contains(basename, ".") && !strings.HasPrefix(basename, ".") && !strings.HasSuffix(basename, ".old") { ac.serve.certMagicDomains = append(ac.serve.certMagicDomains, basename) } } // Using Let's Encrypt implies --domain, to search for suitable directories in the directory to be served ac.serverAddDomain = true } } } // Set the values that has not been set by flags nor scripts (and can be set by both) // Returns true if a "ready function" has been run. func (ac *Config) finalConfiguration(host string) bool { // Set the server host and port (commandline flags overrides Lua configuration) if ac.serverAddr == "" { if ac.serverAddrLua != "" { ac.serverAddr = ac.serverAddrLua } else { ac.serverAddr = utils.JoinHostPort(host, ac.defaultWebColonPort) } } // Set the event server host and port if ac.eventAddr == "" { ac.eventAddr = utils.JoinHostPort(host, ac.defaultEventColonPort) } // Turn off debug mode if production mode is enabled if ac.productionMode { // Turn off debug mode ac.debugMode = false } hasReadyFunction := ac.serverReadyFunctionLua != nil // Run the Lua function specified with the OnReady function, if available if hasReadyFunction { // Useful for outputting configuration information after both // configuration scripts have been run and flags have been parsed ac.serverReadyFunctionLua() } return hasReadyFunction } // dropSelfArg returns args with any entry that refers to the running executable removed. // This guards against Docker images whose ENTRYPOINT is paired with a redundant // CMD ["/bin/algernon"], which Docker appends as a positional argument, causing the // executable path to be picked up as the server directory. func dropSelfArg(args []string) []string { candidates := map[string]struct{}{} add := func(p string) { if p == "" { return } candidates[p] = struct{}{} if abs, err := filepath.Abs(p); err == nil { candidates[abs] = struct{}{} } } if len(os.Args) > 0 { add(os.Args[0]) } if exe, err := os.Executable(); err == nil { add(exe) if resolved, err := filepath.EvalSymlinks(exe); err == nil { add(resolved) } } if len(candidates) == 0 { return args } isSelf := func(arg string) bool { if _, ok := candidates[arg]; ok { return true } if abs, err := filepath.Abs(arg); err == nil { if _, ok := candidates[abs]; ok { return true } } return false } out := args[:0:0] for _, a := range args { if isSelf(a) { continue } out = append(out, a) } return out } // classifyPositionalArgs splits free-form positional arguments into the categories Algernon // historically supported. If pathExists is nil, the serverDir check is skipped. func classifyPositionalArgs(args []string, pathExists func(string) bool) positionalArgs { if pathExists == nil { pathExists = func(string) bool { return false } } looksLikeCertOrKey := func(arg string) bool { switch strings.ToLower(filepath.Ext(arg)) { case ".pem", ".crt", ".cer", ".key": return true default: return false } } looksLikeServerFile := func(arg string) bool { switch strings.ToLower(filepath.Ext(arg)) { case ".lua", ".md", ".markdown", ".zip", ".alg": return true default: return false } } looksLikeRedisAddr := func(arg string) bool { return strings.HasSuffix(arg, ":6379") || strings.HasSuffix(arg, ":6380") } looksLikeWebPort := func(port string) bool { return port == "80" || port == "8080" || strings.HasSuffix(port, "000") } var result positionalArgs for _, arg := range args { if looksLikeCertOrKey(arg) { result.Remaining = append(result.Remaining, arg) continue } if !result.ServerDirSet && (pathExists(arg) || looksLikeServerFile(arg)) { if strings.HasSuffix(arg, string(os.PathSeparator)) { result.ServerDirOrFilename = arg[:len(arg)-1] } else { result.ServerDirOrFilename = arg } result.ServerDirSet = true continue } if looksLikeRedisAddr(arg) { result.RedisAddr = arg result.RedisAddrFromArgs = true continue } // Check if this looks like a host:port address (handles IPv4, IPv6, and port-only) if _, _, err := net.SplitHostPort(arg); err == nil { // Valid host:port format, assume this is a web server address if result.ServerAddr == "" { result.ServerAddr = arg } else if result.ServerAddr2 == "" { result.ServerAddr2 = arg } else { result.Remaining = append(result.Remaining, arg) } continue } if _, err := strconv.Atoi(arg); err == nil { // no error // If in doubt, assume this is the web server port. if looksLikeWebPort(arg) || !result.RedisAddrFromArgs { addr := net.JoinHostPort("", arg) if result.ServerAddr == "" { result.ServerAddr = addr } else if result.ServerAddr2 == "" { result.ServerAddr2 = addr } else { result.Remaining = append(result.Remaining, arg) } } else { result.Remaining = append(result.Remaining, arg) } continue } result.Remaining = append(result.Remaining, arg) } return result }