parser.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. // Copyright 2019 Frédéric Guillot. All rights reserved.
  2. // Use of this source code is governed by the Apache 2.0
  3. // license that can be found in the LICENSE file.
  4. package config // import "miniflux.app/config"
  5. import (
  6. "bufio"
  7. "bytes"
  8. "errors"
  9. "fmt"
  10. "io"
  11. "io/ioutil"
  12. url_parser "net/url"
  13. "os"
  14. "strconv"
  15. "strings"
  16. "miniflux.app/logger"
  17. )
  18. // Parser handles configuration parsing.
  19. type Parser struct {
  20. opts *Options
  21. }
  22. // NewParser returns a new Parser.
  23. func NewParser() *Parser {
  24. return &Parser{
  25. opts: NewOptions(),
  26. }
  27. }
  28. // ParseEnvironmentVariables loads configuration values from environment variables.
  29. func (p *Parser) ParseEnvironmentVariables() (*Options, error) {
  30. err := p.parseLines(os.Environ())
  31. if err != nil {
  32. return nil, err
  33. }
  34. return p.opts, nil
  35. }
  36. // ParseFile loads configuration values from a local file.
  37. func (p *Parser) ParseFile(filename string) (*Options, error) {
  38. fp, err := os.Open(filename)
  39. if err != nil {
  40. return nil, err
  41. }
  42. defer fp.Close()
  43. err = p.parseLines(p.parseFileContent(fp))
  44. if err != nil {
  45. return nil, err
  46. }
  47. return p.opts, nil
  48. }
  49. func (p *Parser) parseFileContent(r io.Reader) (lines []string) {
  50. scanner := bufio.NewScanner(r)
  51. for scanner.Scan() {
  52. line := strings.TrimSpace(scanner.Text())
  53. if len(line) > 0 && !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 {
  54. lines = append(lines, line)
  55. }
  56. }
  57. return lines
  58. }
  59. func (p *Parser) parseLines(lines []string) (err error) {
  60. var port string
  61. for _, line := range lines {
  62. fields := strings.SplitN(line, "=", 2)
  63. key := strings.TrimSpace(fields[0])
  64. value := strings.TrimSpace(fields[1])
  65. switch key {
  66. case "LOG_DATE_TIME":
  67. p.opts.logDateTime = parseBool(value, defaultLogDateTime)
  68. case "DEBUG":
  69. p.opts.debug = parseBool(value, defaultDebug)
  70. case "BASE_URL":
  71. p.opts.baseURL, p.opts.rootURL, p.opts.basePath, err = parseBaseURL(value)
  72. if err != nil {
  73. return err
  74. }
  75. case "PORT":
  76. port = value
  77. case "LISTEN_ADDR":
  78. p.opts.listenAddr = parseString(value, defaultListenAddr)
  79. case "DATABASE_URL":
  80. p.opts.databaseURL = parseString(value, defaultDatabaseURL)
  81. case "DATABASE_URL_FILE":
  82. p.opts.databaseURL = readSecretFile(value, defaultDatabaseURL)
  83. case "DATABASE_MAX_CONNS":
  84. p.opts.databaseMaxConns = parseInt(value, defaultDatabaseMaxConns)
  85. case "DATABASE_MIN_CONNS":
  86. p.opts.databaseMinConns = parseInt(value, defaultDatabaseMinConns)
  87. case "RUN_MIGRATIONS":
  88. p.opts.runMigrations = parseBool(value, defaultRunMigrations)
  89. case "DISABLE_HSTS":
  90. p.opts.hsts = !parseBool(value, defaultHSTS)
  91. case "HTTPS":
  92. p.opts.HTTPS = parseBool(value, defaultHTTPS)
  93. case "DISABLE_SCHEDULER_SERVICE":
  94. p.opts.schedulerService = !parseBool(value, defaultSchedulerService)
  95. case "DISABLE_HTTP_SERVICE":
  96. p.opts.httpService = !parseBool(value, defaultHTTPService)
  97. case "CERT_FILE":
  98. p.opts.certFile = parseString(value, defaultCertFile)
  99. case "KEY_FILE":
  100. p.opts.certKeyFile = parseString(value, defaultKeyFile)
  101. case "CERT_DOMAIN":
  102. p.opts.certDomain = parseString(value, defaultCertDomain)
  103. case "CERT_CACHE":
  104. p.opts.certCache = parseString(value, defaultCertCache)
  105. case "CLEANUP_FREQUENCY_HOURS":
  106. p.opts.cleanupFrequencyHours = parseInt(value, defaultCleanupFrequencyHours)
  107. case "CLEANUP_ARCHIVE_READ_DAYS":
  108. p.opts.cleanupArchiveReadDays = parseInt(value, defaultCleanupArchiveReadDays)
  109. case "CLEANUP_REMOVE_SESSIONS_DAYS":
  110. p.opts.cleanupRemoveSessionsDays = parseInt(value, defaultCleanupRemoveSessionsDays)
  111. case "CLEANUP_FREQUENCY":
  112. logger.Error("[Config] CLEANUP_FREQUENCY has been deprecated in favor of CLEANUP_FREQUENCY_HOURS.")
  113. if p.opts.cleanupFrequencyHours != defaultCleanupFrequencyHours {
  114. logger.Error("[Config] Ignoring CLEANUP_FREQUENCY as CLEANUP_FREQUENCY_HOURS is already specified.")
  115. } else {
  116. p.opts.cleanupFrequencyHours = parseInt(value, defaultCleanupFrequencyHours)
  117. }
  118. case "ARCHIVE_READ_DAYS":
  119. logger.Error("[Config] ARCHIVE_READ_DAYS has been deprecated in favor of CLEANUP_ARCHIVE_READ_DAYS.")
  120. if p.opts.cleanupArchiveReadDays != defaultCleanupArchiveReadDays {
  121. logger.Error("[Config] Ignoring ARCHIVE_READ_DAYS as CLEANUP_ARCHIVE_READ_DAYS is already specified.")
  122. } else {
  123. p.opts.cleanupArchiveReadDays = parseInt(value, defaultCleanupArchiveReadDays)
  124. }
  125. case "WORKER_POOL_SIZE":
  126. p.opts.workerPoolSize = parseInt(value, defaultWorkerPoolSize)
  127. case "POLLING_FREQUENCY":
  128. p.opts.pollingFrequency = parseInt(value, defaultPollingFrequency)
  129. case "BATCH_SIZE":
  130. p.opts.batchSize = parseInt(value, defaultBatchSize)
  131. case "POLLING_SCHEDULER":
  132. p.opts.pollingScheduler = strings.ToLower(parseString(value, defaultPollingScheduler))
  133. case "SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL":
  134. p.opts.schedulerEntryFrequencyMaxInterval = parseInt(value, defaultSchedulerEntryFrequencyMaxInterval)
  135. case "SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL":
  136. p.opts.schedulerEntryFrequencyMinInterval = parseInt(value, defaultSchedulerEntryFrequencyMinInterval)
  137. case "PROXY_IMAGES":
  138. p.opts.proxyImages = parseString(value, defaultProxyImages)
  139. case "CREATE_ADMIN":
  140. p.opts.createAdmin = parseBool(value, defaultCreateAdmin)
  141. case "ADMIN_USERNAME":
  142. p.opts.adminUsername = parseString(value, defaultAdminUsername)
  143. case "ADMIN_USERNAME_FILE":
  144. p.opts.adminUsername = readSecretFile(value, defaultAdminUsername)
  145. case "ADMIN_PASSWORD":
  146. p.opts.adminPassword = parseString(value, defaultAdminPassword)
  147. case "ADMIN_PASSWORD_FILE":
  148. p.opts.adminPassword = readSecretFile(value, defaultAdminPassword)
  149. case "POCKET_CONSUMER_KEY":
  150. p.opts.pocketConsumerKey = parseString(value, defaultPocketConsumerKey)
  151. case "POCKET_CONSUMER_KEY_FILE":
  152. p.opts.pocketConsumerKey = readSecretFile(value, defaultPocketConsumerKey)
  153. case "OAUTH2_USER_CREATION":
  154. p.opts.oauth2UserCreationAllowed = parseBool(value, defaultOAuth2UserCreation)
  155. case "OAUTH2_CLIENT_ID":
  156. p.opts.oauth2ClientID = parseString(value, defaultOAuth2ClientID)
  157. case "OAUTH2_CLIENT_ID_FILE":
  158. p.opts.oauth2ClientID = readSecretFile(value, defaultOAuth2ClientID)
  159. case "OAUTH2_CLIENT_SECRET":
  160. p.opts.oauth2ClientSecret = parseString(value, defaultOAuth2ClientSecret)
  161. case "OAUTH2_CLIENT_SECRET_FILE":
  162. p.opts.oauth2ClientSecret = readSecretFile(value, defaultOAuth2ClientSecret)
  163. case "OAUTH2_REDIRECT_URL":
  164. p.opts.oauth2RedirectURL = parseString(value, defaultOAuth2RedirectURL)
  165. case "OAUTH2_OIDC_DISCOVERY_ENDPOINT":
  166. p.opts.oauth2OidcDiscoveryEndpoint = parseString(value, defaultOAuth2OidcDiscoveryEndpoint)
  167. case "OAUTH2_PROVIDER":
  168. p.opts.oauth2Provider = parseString(value, defaultOAuth2Provider)
  169. case "HTTP_CLIENT_TIMEOUT":
  170. p.opts.httpClientTimeout = parseInt(value, defaultHTTPClientTimeout)
  171. case "HTTP_CLIENT_MAX_BODY_SIZE":
  172. p.opts.httpClientMaxBodySize = int64(parseInt(value, defaultHTTPClientMaxBodySize) * 1024 * 1024)
  173. case "AUTH_PROXY_HEADER":
  174. p.opts.authProxyHeader = parseString(value, defaultAuthProxyHeader)
  175. case "AUTH_PROXY_USER_CREATION":
  176. p.opts.authProxyUserCreation = parseBool(value, defaultAuthProxyUserCreation)
  177. }
  178. }
  179. if port != "" {
  180. p.opts.listenAddr = ":" + port
  181. }
  182. return nil
  183. }
  184. func parseBaseURL(value string) (string, string, string, error) {
  185. if value == "" {
  186. return defaultBaseURL, defaultRootURL, "", nil
  187. }
  188. if value[len(value)-1:] == "/" {
  189. value = value[:len(value)-1]
  190. }
  191. url, err := url_parser.Parse(value)
  192. if err != nil {
  193. return "", "", "", fmt.Errorf("Invalid BASE_URL: %v", err)
  194. }
  195. scheme := strings.ToLower(url.Scheme)
  196. if scheme != "https" && scheme != "http" {
  197. return "", "", "", errors.New("Invalid BASE_URL: scheme must be http or https")
  198. }
  199. basePath := url.Path
  200. url.Path = ""
  201. return value, url.String(), basePath, nil
  202. }
  203. func parseBool(value string, fallback bool) bool {
  204. if value == "" {
  205. return fallback
  206. }
  207. value = strings.ToLower(value)
  208. if value == "1" || value == "yes" || value == "true" || value == "on" {
  209. return true
  210. }
  211. return false
  212. }
  213. func parseInt(value string, fallback int) int {
  214. if value == "" {
  215. return fallback
  216. }
  217. v, err := strconv.Atoi(value)
  218. if err != nil {
  219. return fallback
  220. }
  221. return v
  222. }
  223. func parseString(value string, fallback string) string {
  224. if value == "" {
  225. return fallback
  226. }
  227. return value
  228. }
  229. func readSecretFile(filename, fallback string) string {
  230. data, err := ioutil.ReadFile(filename)
  231. if err != nil {
  232. return fallback
  233. }
  234. value := string(bytes.TrimSpace(data))
  235. if value == "" {
  236. return fallback
  237. }
  238. return value
  239. }