cli.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package cli // import "miniflux.app/v2/internal/cli"
  4. import (
  5. "errors"
  6. "flag"
  7. "fmt"
  8. "io"
  9. "log/slog"
  10. "os"
  11. "miniflux.app/v2/internal/botauth"
  12. "miniflux.app/v2/internal/config"
  13. "miniflux.app/v2/internal/database"
  14. "miniflux.app/v2/internal/proxyrotator"
  15. "miniflux.app/v2/internal/storage"
  16. "miniflux.app/v2/internal/ui/static"
  17. "miniflux.app/v2/internal/version"
  18. )
  19. const (
  20. flagInfoHelp = "Show build information"
  21. flagVersionHelp = "Show application version"
  22. flagMigrateHelp = "Run SQL migrations"
  23. flagFlushSessionsHelp = "Flush all sessions (disconnect users)"
  24. flagCreateAdminHelp = "Create an admin user from an interactive terminal"
  25. flagResetPasswordHelp = "Reset user password"
  26. flagResetFeedErrorsHelp = "Clear all feed errors for all users"
  27. flagDebugModeHelp = "Show debug logs"
  28. flagConfigFileHelp = "Load configuration file"
  29. flagConfigDumpHelp = "Print parsed configuration values"
  30. flagHealthCheckHelp = `Perform a health check on the given endpoint (the value "auto" try to guess the health check endpoint).`
  31. flagRefreshFeedsHelp = "Refresh a batch of feeds and exit"
  32. flagRunCleanupTasksHelp = "Run cleanup tasks (delete old sessions and archives old entries)"
  33. flagExportUserFeedsHelp = "Export user feeds (provide the username as argument)"
  34. flagResetNextCheckAtHelp = "Reset the next check time for all feeds"
  35. flagGenerateNewBotKeysHelp = "Generate a new Ed25519 key pair for web bot authentication and exit"
  36. )
  37. // Parse parses command line arguments.
  38. func Parse() {
  39. var (
  40. err error
  41. flagInfo bool
  42. flagVersion bool
  43. flagMigrate bool
  44. flagFlushSessions bool
  45. flagCreateAdmin bool
  46. flagResetPassword bool
  47. flagResetFeedErrors bool
  48. flagResetFeedNextCheckAt bool
  49. flagDebugMode bool
  50. flagConfigFile string
  51. flagConfigDump bool
  52. flagHealthCheck string
  53. flagRefreshFeeds bool
  54. flagRunCleanupTasks bool
  55. flagExportUserFeeds string
  56. flagGenerateNewBotKeys bool
  57. )
  58. flag.BoolVar(&flagInfo, "info", false, flagInfoHelp)
  59. flag.BoolVar(&flagInfo, "i", false, flagInfoHelp)
  60. flag.BoolVar(&flagVersion, "version", false, flagVersionHelp)
  61. flag.BoolVar(&flagVersion, "v", false, flagVersionHelp)
  62. flag.BoolVar(&flagMigrate, "migrate", false, flagMigrateHelp)
  63. flag.BoolVar(&flagFlushSessions, "flush-sessions", false, flagFlushSessionsHelp)
  64. flag.BoolVar(&flagCreateAdmin, "create-admin", false, flagCreateAdminHelp)
  65. flag.BoolVar(&flagResetPassword, "reset-password", false, flagResetPasswordHelp)
  66. flag.BoolVar(&flagResetFeedErrors, "reset-feed-errors", false, flagResetFeedErrorsHelp)
  67. flag.BoolVar(&flagResetFeedNextCheckAt, "reset-feed-next-check-at", false, flagResetNextCheckAtHelp)
  68. flag.BoolVar(&flagDebugMode, "debug", false, flagDebugModeHelp)
  69. flag.StringVar(&flagConfigFile, "config-file", "", flagConfigFileHelp)
  70. flag.StringVar(&flagConfigFile, "c", "", flagConfigFileHelp)
  71. flag.BoolVar(&flagConfigDump, "config-dump", false, flagConfigDumpHelp)
  72. flag.StringVar(&flagHealthCheck, "healthcheck", "", flagHealthCheckHelp)
  73. flag.BoolVar(&flagRefreshFeeds, "refresh-feeds", false, flagRefreshFeedsHelp)
  74. flag.BoolVar(&flagRunCleanupTasks, "run-cleanup-tasks", false, flagRunCleanupTasksHelp)
  75. flag.StringVar(&flagExportUserFeeds, "export-user-feeds", "", flagExportUserFeedsHelp)
  76. flag.BoolVar(&flagGenerateNewBotKeys, "generate-new-bot-keys", false, flagGenerateNewBotKeysHelp)
  77. flag.Parse()
  78. cfg := config.NewConfigParser()
  79. if flagConfigFile != "" {
  80. config.Opts, err = cfg.ParseFile(flagConfigFile)
  81. if err != nil {
  82. printErrorAndExit(err)
  83. }
  84. }
  85. config.Opts, err = cfg.ParseEnvironmentVariables()
  86. if err != nil {
  87. printErrorAndExit(err)
  88. }
  89. if oauth2Provider := config.Opts.OAuth2Provider(); oauth2Provider != "" {
  90. if oauth2Provider != "oidc" && oauth2Provider != "google" {
  91. printErrorAndExit(fmt.Errorf(`unsupported OAuth2 provider: %q (Possible values are "google" or "oidc")`, oauth2Provider))
  92. }
  93. }
  94. if config.Opts.DisableLocalAuth() {
  95. switch {
  96. case config.Opts.OAuth2Provider() == "" && config.Opts.AuthProxyHeader() == "":
  97. printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled but neither OAUTH2_PROVIDER nor AUTH_PROXY_HEADER is not set. Please enable at least one authentication source"))
  98. case config.Opts.OAuth2Provider() != "" && !config.Opts.IsOAuth2UserCreationAllowed():
  99. printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled and an OAUTH2_PROVIDER is configured, but OAUTH2_USER_CREATION is not enabled"))
  100. case config.Opts.AuthProxyHeader() != "" && !config.Opts.IsAuthProxyUserCreationAllowed():
  101. printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled and an AUTH_PROXY_HEADER is configured, but AUTH_PROXY_USER_CREATION is not enabled"))
  102. }
  103. }
  104. if flagConfigDump {
  105. fmt.Print(config.Opts)
  106. return
  107. }
  108. if flagDebugMode {
  109. config.Opts.SetLogLevel("debug")
  110. }
  111. logFile := config.Opts.LogFile()
  112. var logFileHandler io.Writer
  113. switch logFile {
  114. case "stdout":
  115. logFileHandler = os.Stdout
  116. case "stderr":
  117. logFileHandler = os.Stderr
  118. default:
  119. logFileHandler, err = os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
  120. if err != nil {
  121. printErrorAndExit(fmt.Errorf("unable to open log file: %v", err))
  122. }
  123. defer logFileHandler.(*os.File).Close()
  124. }
  125. if err := InitializeDefaultLogger(config.Opts.LogLevel(), logFileHandler, config.Opts.LogFormat(), config.Opts.LogDateTime()); err != nil {
  126. printErrorAndExit(err)
  127. }
  128. if flagHealthCheck != "" {
  129. doHealthCheck(flagHealthCheck)
  130. return
  131. }
  132. if flagInfo {
  133. info()
  134. return
  135. }
  136. if flagVersion {
  137. fmt.Println(version.Version)
  138. return
  139. }
  140. if config.Opts.IsDefaultDatabaseURL() {
  141. slog.Info("The default value for DATABASE_URL is used")
  142. }
  143. if err := static.GenerateBinaryBundles(); err != nil {
  144. printErrorAndExit(fmt.Errorf("unable to generate binary files bundle: %v", err))
  145. }
  146. if err := static.GenerateStylesheetsBundles(); err != nil {
  147. printErrorAndExit(fmt.Errorf("unable to generate stylesheets bundle: %v", err))
  148. }
  149. if err := static.GenerateJavascriptBundles(config.Opts.WebAuthn()); err != nil {
  150. printErrorAndExit(fmt.Errorf("unable to generate javascript bundle: %v", err))
  151. }
  152. db, err := database.NewConnectionPool(
  153. config.Opts.DatabaseURL(),
  154. config.Opts.DatabaseMinConns(),
  155. config.Opts.DatabaseMaxConns(),
  156. config.Opts.DatabaseConnectionLifetime(),
  157. )
  158. if err != nil {
  159. printErrorAndExit(fmt.Errorf("unable to connect to database: %v", err))
  160. }
  161. defer db.Close()
  162. store := storage.NewStorage(db)
  163. if err := store.Ping(); err != nil {
  164. printErrorAndExit(err)
  165. }
  166. if flagMigrate {
  167. if err := database.Migrate(db); err != nil {
  168. printErrorAndExit(err)
  169. }
  170. return
  171. }
  172. if flagResetFeedErrors {
  173. if err := store.ResetFeedErrors(); err != nil {
  174. printErrorAndExit(err)
  175. }
  176. return
  177. }
  178. if flagResetFeedNextCheckAt {
  179. if err := store.ResetNextCheckAt(); err != nil {
  180. printErrorAndExit(err)
  181. }
  182. return
  183. }
  184. if flagExportUserFeeds != "" {
  185. exportUserFeeds(store, flagExportUserFeeds)
  186. return
  187. }
  188. if flagFlushSessions {
  189. flushSessions(store)
  190. return
  191. }
  192. if flagCreateAdmin {
  193. createAdminUserFromInteractiveTerminal(store)
  194. return
  195. }
  196. if flagResetPassword {
  197. resetPassword(store)
  198. return
  199. }
  200. if flagGenerateNewBotKeys {
  201. slog.Info("Generating a new Ed25519 key pair for web bot authentication")
  202. if err := store.CreateWebAuthBothKeys(); err != nil {
  203. printErrorAndExit(fmt.Errorf("unable to create web bot auth keys: %v", err))
  204. }
  205. slog.Info("A new Ed25519 key pair has been generated for web bot authentication")
  206. return
  207. }
  208. // Run migrations and start the daemon.
  209. if config.Opts.RunMigrations() {
  210. if err := database.Migrate(db); err != nil {
  211. printErrorAndExit(err)
  212. }
  213. }
  214. if err := database.IsSchemaUpToDate(db); err != nil {
  215. printErrorAndExit(err)
  216. }
  217. if config.Opts.CreateAdmin() {
  218. createAdminUserFromEnvironmentVariables(store)
  219. }
  220. if config.Opts.HasHTTPClientProxiesConfigured() {
  221. slog.Info("Initializing proxy rotation", slog.Int("proxies_count", len(config.Opts.HTTPClientProxies())))
  222. proxyrotator.ProxyRotatorInstance, err = proxyrotator.NewProxyRotator(config.Opts.HTTPClientProxies())
  223. if err != nil {
  224. printErrorAndExit(fmt.Errorf("unable to initialize proxy rotator: %v", err))
  225. }
  226. }
  227. if config.Opts.WebBotAuth() {
  228. hasKeys, err := store.HasWebAuthBothKeys()
  229. if err != nil {
  230. printErrorAndExit(fmt.Errorf("unable to check for existing web bot auth keys: %v", err))
  231. }
  232. if !hasKeys {
  233. slog.Info("Web bot authentication is enabled but no keys are present in the database, generating a new key pair")
  234. if err := store.CreateWebAuthBothKeys(); err != nil {
  235. printErrorAndExit(fmt.Errorf("unable to create web bot auth keys: %v", err))
  236. }
  237. slog.Info("A new Ed25519 key pair has been generated for web bot authentication")
  238. }
  239. keys, err := store.WebAuthBothKeys()
  240. if err != nil {
  241. printErrorAndExit(fmt.Errorf("unable to fetch web bot auth keys: %v", err))
  242. }
  243. botauth.GlobalInstance, err = botauth.NewBothAuth(
  244. config.Opts.BaseURL(),
  245. keys,
  246. )
  247. if err != nil {
  248. printErrorAndExit(fmt.Errorf("unable to initialize web bot auth: %v", err))
  249. }
  250. slog.Info("Web bot authentication is enabled", slog.String("directory_url", botauth.GlobalInstance.DirectoryURL()))
  251. }
  252. if flagRefreshFeeds {
  253. refreshFeeds(store)
  254. return
  255. }
  256. if flagRunCleanupTasks {
  257. runCleanupTasks(store)
  258. return
  259. }
  260. startDaemon(store)
  261. }
  262. func printErrorAndExit(err error) {
  263. fmt.Fprintln(os.Stderr, err)
  264. os.Exit(1)
  265. }