parser.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package config // import "miniflux.app/v2/internal/config"
  4. import (
  5. "bufio"
  6. "bytes"
  7. "crypto/rand"
  8. "errors"
  9. "fmt"
  10. "io"
  11. "net/url"
  12. "os"
  13. "strconv"
  14. "strings"
  15. "time"
  16. )
  17. // parser handles configuration parsing.
  18. type parser struct {
  19. opts *options
  20. }
  21. // NewParser returns a new Parser.
  22. func NewParser() *parser {
  23. return &parser{
  24. opts: NewOptions(),
  25. }
  26. }
  27. // ParseEnvironmentVariables loads configuration values from environment variables.
  28. func (p *parser) ParseEnvironmentVariables() (*options, error) {
  29. err := p.parseLines(os.Environ())
  30. if err != nil {
  31. return nil, err
  32. }
  33. return p.opts, nil
  34. }
  35. // ParseFile loads configuration values from a local file.
  36. func (p *parser) ParseFile(filename string) (*options, error) {
  37. fp, err := os.Open(filename)
  38. if err != nil {
  39. return nil, err
  40. }
  41. defer fp.Close()
  42. err = p.parseLines(p.parseFileContent(fp))
  43. if err != nil {
  44. return nil, err
  45. }
  46. return p.opts, nil
  47. }
  48. func (p *parser) parseFileContent(r io.Reader) (lines []string) {
  49. scanner := bufio.NewScanner(r)
  50. for scanner.Scan() {
  51. line := strings.TrimSpace(scanner.Text())
  52. if !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 {
  53. lines = append(lines, line)
  54. }
  55. }
  56. return lines
  57. }
  58. func (p *parser) parseLines(lines []string) (err error) {
  59. var port string
  60. for lineNum, line := range lines {
  61. key, value, ok := strings.Cut(line, "=")
  62. if !ok {
  63. return fmt.Errorf("config: unable to parse configuration, invalid format on line %d", lineNum)
  64. }
  65. key, value = strings.TrimSpace(key), strings.TrimSpace(value)
  66. switch key {
  67. case "LOG_FILE":
  68. p.opts.logFile = parseString(value, defaultLogFile)
  69. case "LOG_DATE_TIME":
  70. p.opts.logDateTime = parseBool(value, defaultLogDateTime)
  71. case "LOG_LEVEL":
  72. parsedValue := parseString(value, defaultLogLevel)
  73. if parsedValue == "debug" || parsedValue == "info" || parsedValue == "warning" || parsedValue == "error" {
  74. p.opts.logLevel = parsedValue
  75. }
  76. case "LOG_FORMAT":
  77. parsedValue := parseString(value, defaultLogFormat)
  78. if parsedValue == "json" || parsedValue == "text" {
  79. p.opts.logFormat = parsedValue
  80. }
  81. case "BASE_URL":
  82. p.opts.baseURL, p.opts.rootURL, p.opts.basePath, err = parseBaseURL(value)
  83. if err != nil {
  84. return err
  85. }
  86. case "PORT":
  87. port = value
  88. case "LISTEN_ADDR":
  89. p.opts.listenAddr = parseStringList(value, []string{defaultListenAddr})
  90. case "DATABASE_URL":
  91. p.opts.databaseURL = parseString(value, defaultDatabaseURL)
  92. case "DATABASE_URL_FILE":
  93. p.opts.databaseURL = readSecretFile(value, defaultDatabaseURL)
  94. case "DATABASE_MAX_CONNS":
  95. p.opts.databaseMaxConns = parseInt(value, defaultDatabaseMaxConns)
  96. case "DATABASE_MIN_CONNS":
  97. p.opts.databaseMinConns = parseInt(value, defaultDatabaseMinConns)
  98. case "DATABASE_CONNECTION_LIFETIME":
  99. p.opts.databaseConnectionLifetime = parseInt(value, defaultDatabaseConnectionLifetime)
  100. case "FILTER_ENTRY_MAX_AGE_DAYS":
  101. p.opts.filterEntryMaxAgeDays = parseInt(value, defaultFilterEntryMaxAgeDays)
  102. case "RUN_MIGRATIONS":
  103. p.opts.runMigrations = parseBool(value, defaultRunMigrations)
  104. case "DISABLE_HSTS":
  105. p.opts.hsts = !parseBool(value, defaultHSTS)
  106. case "HTTPS":
  107. p.opts.HTTPS = parseBool(value, defaultHTTPS)
  108. case "DISABLE_SCHEDULER_SERVICE":
  109. p.opts.schedulerService = !parseBool(value, defaultSchedulerService)
  110. case "DISABLE_HTTP_SERVICE":
  111. p.opts.httpService = !parseBool(value, defaultHTTPService)
  112. case "CERT_FILE":
  113. p.opts.certFile = parseString(value, defaultCertFile)
  114. case "KEY_FILE":
  115. p.opts.certKeyFile = parseString(value, defaultKeyFile)
  116. case "CERT_DOMAIN":
  117. p.opts.certDomain = parseString(value, defaultCertDomain)
  118. case "CLEANUP_FREQUENCY_HOURS":
  119. p.opts.cleanupFrequencyHours = parseInt(value, defaultCleanupFrequencyHours)
  120. case "CLEANUP_ARCHIVE_READ_DAYS":
  121. p.opts.cleanupArchiveReadDays = parseInt(value, defaultCleanupArchiveReadDays)
  122. case "CLEANUP_ARCHIVE_UNREAD_DAYS":
  123. p.opts.cleanupArchiveUnreadDays = parseInt(value, defaultCleanupArchiveUnreadDays)
  124. case "CLEANUP_ARCHIVE_BATCH_SIZE":
  125. p.opts.cleanupArchiveBatchSize = parseInt(value, defaultCleanupArchiveBatchSize)
  126. case "CLEANUP_REMOVE_SESSIONS_DAYS":
  127. p.opts.cleanupRemoveSessionsDays = parseInt(value, defaultCleanupRemoveSessionsDays)
  128. case "WORKER_POOL_SIZE":
  129. p.opts.workerPoolSize = parseInt(value, defaultWorkerPoolSize)
  130. case "FORCE_REFRESH_INTERVAL":
  131. p.opts.forceRefreshInterval = parseInterval(value, time.Second, defaultForceRefreshInterval)
  132. case "BATCH_SIZE":
  133. p.opts.batchSize = parseInt(value, defaultBatchSize)
  134. case "POLLING_FREQUENCY":
  135. p.opts.pollingFrequency = parseInt(value, defaultPollingFrequency)
  136. case "POLLING_LIMIT_PER_HOST":
  137. p.opts.pollingLimitPerHost = parseInt(value, 0)
  138. case "POLLING_PARSING_ERROR_LIMIT":
  139. p.opts.pollingParsingErrorLimit = parseInt(value, defaultPollingParsingErrorLimit)
  140. case "POLLING_SCHEDULER":
  141. p.opts.pollingScheduler = strings.ToLower(parseString(value, defaultPollingScheduler))
  142. case "SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL":
  143. p.opts.schedulerEntryFrequencyMaxInterval = parseInterval(value, time.Minute, defaultSchedulerEntryFrequencyMaxInterval)
  144. case "SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL":
  145. p.opts.schedulerEntryFrequencyMinInterval = parseInterval(value, time.Minute, defaultSchedulerEntryFrequencyMinInterval)
  146. case "SCHEDULER_ENTRY_FREQUENCY_FACTOR":
  147. p.opts.schedulerEntryFrequencyFactor = parseInt(value, defaultSchedulerEntryFrequencyFactor)
  148. case "SCHEDULER_ROUND_ROBIN_MIN_INTERVAL":
  149. p.opts.schedulerRoundRobinMinInterval = parseInterval(value, time.Minute, defaultSchedulerRoundRobinMinInterval)
  150. case "SCHEDULER_ROUND_ROBIN_MAX_INTERVAL":
  151. p.opts.schedulerRoundRobinMaxInterval = parseInterval(value, time.Minute, defaultSchedulerRoundRobinMaxInterval)
  152. case "MEDIA_PROXY_HTTP_CLIENT_TIMEOUT":
  153. p.opts.mediaProxyHTTPClientTimeout = parseInterval(value, time.Second, defaultMediaProxyHTTPClientTimeout)
  154. case "MEDIA_PROXY_MODE":
  155. p.opts.mediaProxyMode = parseString(value, defaultMediaProxyMode)
  156. case "MEDIA_PROXY_RESOURCE_TYPES":
  157. p.opts.mediaProxyResourceTypes = parseStringList(value, []string{defaultMediaResourceTypes})
  158. case "MEDIA_PROXY_PRIVATE_KEY":
  159. randomKey := make([]byte, 16)
  160. rand.Read(randomKey)
  161. p.opts.mediaProxyPrivateKey = parseBytes(value, randomKey)
  162. case "MEDIA_PROXY_CUSTOM_URL":
  163. p.opts.mediaProxyCustomURL, err = url.Parse(parseString(value, defaultMediaProxyURL))
  164. if err != nil {
  165. return fmt.Errorf("config: invalid MEDIA_PROXY_CUSTOM_URL value: %w", err)
  166. }
  167. case "CREATE_ADMIN":
  168. p.opts.createAdmin = parseBool(value, defaultCreateAdmin)
  169. case "ADMIN_USERNAME":
  170. p.opts.adminUsername = parseString(value, defaultAdminUsername)
  171. case "ADMIN_USERNAME_FILE":
  172. p.opts.adminUsername = readSecretFile(value, defaultAdminUsername)
  173. case "ADMIN_PASSWORD":
  174. p.opts.adminPassword = parseString(value, defaultAdminPassword)
  175. case "ADMIN_PASSWORD_FILE":
  176. p.opts.adminPassword = readSecretFile(value, defaultAdminPassword)
  177. case "OAUTH2_USER_CREATION":
  178. p.opts.oauth2UserCreationAllowed = parseBool(value, defaultOAuth2UserCreation)
  179. case "OAUTH2_CLIENT_ID":
  180. p.opts.oauth2ClientID = parseString(value, defaultOAuth2ClientID)
  181. case "OAUTH2_CLIENT_ID_FILE":
  182. p.opts.oauth2ClientID = readSecretFile(value, defaultOAuth2ClientID)
  183. case "OAUTH2_CLIENT_SECRET":
  184. p.opts.oauth2ClientSecret = parseString(value, defaultOAuth2ClientSecret)
  185. case "OAUTH2_CLIENT_SECRET_FILE":
  186. p.opts.oauth2ClientSecret = readSecretFile(value, defaultOAuth2ClientSecret)
  187. case "OAUTH2_REDIRECT_URL":
  188. p.opts.oauth2RedirectURL = parseString(value, defaultOAuth2RedirectURL)
  189. case "OAUTH2_OIDC_DISCOVERY_ENDPOINT":
  190. p.opts.oidcDiscoveryEndpoint = parseString(value, defaultOAuth2OidcDiscoveryEndpoint)
  191. case "OAUTH2_OIDC_PROVIDER_NAME":
  192. p.opts.oidcProviderName = parseString(value, defaultOauth2OidcProviderName)
  193. case "OAUTH2_PROVIDER":
  194. p.opts.oauth2Provider = parseString(value, defaultOAuth2Provider)
  195. case "DISABLE_LOCAL_AUTH":
  196. p.opts.disableLocalAuth = parseBool(value, defaultDisableLocalAuth)
  197. case "HTTP_CLIENT_TIMEOUT":
  198. p.opts.httpClientTimeout = parseInterval(value, time.Second, defaultHTTPClientTimeout)
  199. case "HTTP_CLIENT_MAX_BODY_SIZE":
  200. p.opts.httpClientMaxBodySize = int64(parseInt(value, defaultHTTPClientMaxBodySize) * 1024 * 1024)
  201. case "HTTP_CLIENT_PROXY":
  202. p.opts.httpClientProxyURL, err = url.Parse(parseString(value, defaultHTTPClientProxy))
  203. if err != nil {
  204. return fmt.Errorf("config: invalid HTTP_CLIENT_PROXY value: %w", err)
  205. }
  206. case "HTTP_CLIENT_PROXIES":
  207. p.opts.httpClientProxies = parseStringList(value, []string{})
  208. case "HTTP_CLIENT_USER_AGENT":
  209. p.opts.httpClientUserAgent = parseString(value, defaultHTTPClientUserAgent)
  210. case "HTTP_SERVER_TIMEOUT":
  211. p.opts.httpServerTimeout = parseInterval(value, time.Second, defaultHTTPServerTimeout)
  212. case "AUTH_PROXY_HEADER":
  213. p.opts.authProxyHeader = parseString(value, defaultAuthProxyHeader)
  214. case "AUTH_PROXY_USER_CREATION":
  215. p.opts.authProxyUserCreation = parseBool(value, defaultAuthProxyUserCreation)
  216. case "MAINTENANCE_MODE":
  217. p.opts.maintenanceMode = parseBool(value, defaultMaintenanceMode)
  218. case "MAINTENANCE_MESSAGE":
  219. p.opts.maintenanceMessage = parseString(value, defaultMaintenanceMessage)
  220. case "METRICS_COLLECTOR":
  221. p.opts.metricsCollector = parseBool(value, defaultMetricsCollector)
  222. case "METRICS_REFRESH_INTERVAL":
  223. p.opts.metricsRefreshInterval = parseInterval(value, time.Second, defaultMetricsRefreshInterval)
  224. case "METRICS_ALLOWED_NETWORKS":
  225. p.opts.metricsAllowedNetworks = parseStringList(value, []string{defaultMetricsAllowedNetworks})
  226. case "METRICS_USERNAME":
  227. p.opts.metricsUsername = parseString(value, defaultMetricsUsername)
  228. case "METRICS_USERNAME_FILE":
  229. p.opts.metricsUsername = readSecretFile(value, defaultMetricsUsername)
  230. case "METRICS_PASSWORD":
  231. p.opts.metricsPassword = parseString(value, defaultMetricsPassword)
  232. case "METRICS_PASSWORD_FILE":
  233. p.opts.metricsPassword = readSecretFile(value, defaultMetricsPassword)
  234. case "FETCH_BILIBILI_WATCH_TIME":
  235. p.opts.fetchBilibiliWatchTime = parseBool(value, defaultFetchBilibiliWatchTime)
  236. case "FETCH_NEBULA_WATCH_TIME":
  237. p.opts.fetchNebulaWatchTime = parseBool(value, defaultFetchNebulaWatchTime)
  238. case "FETCH_ODYSEE_WATCH_TIME":
  239. p.opts.fetchOdyseeWatchTime = parseBool(value, defaultFetchOdyseeWatchTime)
  240. case "FETCH_YOUTUBE_WATCH_TIME":
  241. p.opts.fetchYouTubeWatchTime = parseBool(value, defaultFetchYouTubeWatchTime)
  242. case "YOUTUBE_API_KEY":
  243. p.opts.youTubeApiKey = parseString(value, defaultYouTubeApiKey)
  244. case "YOUTUBE_EMBED_URL_OVERRIDE":
  245. p.opts.youTubeEmbedUrlOverride = parseString(value, defaultYouTubeEmbedUrlOverride)
  246. case "WATCHDOG":
  247. p.opts.watchdog = parseBool(value, defaultWatchdog)
  248. case "INVIDIOUS_INSTANCE":
  249. p.opts.invidiousInstance = parseString(value, defaultInvidiousInstance)
  250. case "WEBAUTHN":
  251. p.opts.webAuthn = parseBool(value, defaultWebAuthn)
  252. }
  253. }
  254. if port != "" {
  255. p.opts.listenAddr = []string{":" + port}
  256. }
  257. youtubeEmbedURL, err := url.Parse(p.opts.youTubeEmbedUrlOverride)
  258. if err != nil {
  259. return fmt.Errorf("config: invalid YOUTUBE_EMBED_URL_OVERRIDE value: %w", err)
  260. }
  261. p.opts.youTubeEmbedDomain = youtubeEmbedURL.Hostname()
  262. return nil
  263. }
  264. func parseBaseURL(value string) (string, string, string, error) {
  265. if value == "" {
  266. return defaultBaseURL, defaultRootURL, "", nil
  267. }
  268. value = strings.TrimSuffix(value, "/")
  269. parsedURL, err := url.Parse(value)
  270. if err != nil {
  271. return "", "", "", fmt.Errorf("config: invalid BASE_URL: %w", err)
  272. }
  273. scheme := strings.ToLower(parsedURL.Scheme)
  274. if scheme != "https" && scheme != "http" {
  275. return "", "", "", errors.New("config: invalid BASE_URL: scheme must be http or https")
  276. }
  277. basePath := parsedURL.Path
  278. parsedURL.Path = ""
  279. return value, parsedURL.String(), basePath, nil
  280. }
  281. func parseBool(value string, fallback bool) bool {
  282. if value == "" {
  283. return fallback
  284. }
  285. value = strings.ToLower(value)
  286. if value == "1" || value == "yes" || value == "true" || value == "on" {
  287. return true
  288. }
  289. return false
  290. }
  291. func parseInt(value string, fallback int) int {
  292. if value == "" {
  293. return fallback
  294. }
  295. v, err := strconv.Atoi(value)
  296. if err != nil {
  297. return fallback
  298. }
  299. return v
  300. }
  301. func parseString(value string, fallback string) string {
  302. if value == "" {
  303. return fallback
  304. }
  305. return value
  306. }
  307. func parseStringList(value string, fallback []string) []string {
  308. if value == "" {
  309. return fallback
  310. }
  311. var strList []string
  312. present := make(map[string]bool)
  313. for item := range strings.SplitSeq(value, ",") {
  314. if itemValue := strings.TrimSpace(item); itemValue != "" {
  315. if !present[itemValue] {
  316. present[itemValue] = true
  317. strList = append(strList, itemValue)
  318. }
  319. }
  320. }
  321. return strList
  322. }
  323. func parseBytes(value string, fallback []byte) []byte {
  324. if value == "" {
  325. return fallback
  326. }
  327. return []byte(value)
  328. }
  329. // parseInterval converts an integer "value" to [time.Duration] using "unit" as multiplier.
  330. func parseInterval(value string, unit time.Duration, fallback time.Duration) time.Duration {
  331. if value == "" {
  332. return fallback
  333. }
  334. v, err := strconv.Atoi(value)
  335. if err != nil {
  336. return fallback
  337. }
  338. return time.Duration(v) * unit
  339. }
  340. func readSecretFile(filename, fallback string) string {
  341. data, err := os.ReadFile(filename)
  342. if err != nil {
  343. return fallback
  344. }
  345. value := string(bytes.TrimSpace(data))
  346. if value == "" {
  347. return fallback
  348. }
  349. return value
  350. }