2019-06-01 21:18:09 -04:00
|
|
|
// Copyright 2019 Frédéric Guillot. All rights reserved.
|
|
|
|
// Use of this source code is governed by the Apache 2.0
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
package config // import "miniflux.app/config"
|
|
|
|
|
|
|
|
import (
|
2019-06-02 21:20:59 -04:00
|
|
|
"bufio"
|
2020-06-29 23:49:05 -04:00
|
|
|
"bytes"
|
2019-06-01 21:18:09 -04:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2019-06-02 21:20:59 -04:00
|
|
|
"io"
|
2020-06-29 23:49:05 -04:00
|
|
|
"io/ioutil"
|
2019-06-02 21:20:59 -04:00
|
|
|
url_parser "net/url"
|
2019-06-01 21:18:09 -04:00
|
|
|
"os"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
2019-06-02 21:20:59 -04:00
|
|
|
// Parser handles configuration parsing.
|
|
|
|
type Parser struct {
|
|
|
|
opts *Options
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewParser returns a new Parser.
|
|
|
|
func NewParser() *Parser {
|
|
|
|
return &Parser{
|
|
|
|
opts: NewOptions(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ParseEnvironmentVariables loads configuration values from environment variables.
|
|
|
|
func (p *Parser) ParseEnvironmentVariables() (*Options, error) {
|
|
|
|
err := p.parseLines(os.Environ())
|
2019-06-01 21:18:09 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-06-02 21:20:59 -04:00
|
|
|
return p.opts, nil
|
|
|
|
}
|
2019-06-01 21:18:09 -04:00
|
|
|
|
2019-06-02 21:20:59 -04:00
|
|
|
// ParseFile loads configuration values from a local file.
|
|
|
|
func (p *Parser) ParseFile(filename string) (*Options, error) {
|
|
|
|
fp, err := os.Open(filename)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer fp.Close()
|
2019-06-01 21:18:09 -04:00
|
|
|
|
2019-06-02 21:20:59 -04:00
|
|
|
err = p.parseLines(p.parseFileContent(fp))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return p.opts, nil
|
|
|
|
}
|
2019-06-01 21:18:09 -04:00
|
|
|
|
2019-06-02 21:20:59 -04:00
|
|
|
func (p *Parser) parseFileContent(r io.Reader) (lines []string) {
|
|
|
|
scanner := bufio.NewScanner(r)
|
|
|
|
for scanner.Scan() {
|
|
|
|
line := strings.TrimSpace(scanner.Text())
|
|
|
|
if len(line) > 0 && !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 {
|
|
|
|
lines = append(lines, line)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return lines
|
|
|
|
}
|
2019-06-01 21:18:09 -04:00
|
|
|
|
2019-06-02 21:20:59 -04:00
|
|
|
func (p *Parser) parseLines(lines []string) (err error) {
|
|
|
|
var port string
|
|
|
|
|
|
|
|
for _, line := range lines {
|
|
|
|
fields := strings.SplitN(line, "=", 2)
|
|
|
|
key := strings.TrimSpace(fields[0])
|
|
|
|
value := strings.TrimSpace(fields[1])
|
|
|
|
|
|
|
|
switch key {
|
2019-06-08 20:16:12 -04:00
|
|
|
case "LOG_DATE_TIME":
|
|
|
|
p.opts.logDateTime = parseBool(value, defaultLogDateTime)
|
2019-06-02 21:20:59 -04:00
|
|
|
case "DEBUG":
|
|
|
|
p.opts.debug = parseBool(value, defaultDebug)
|
|
|
|
case "BASE_URL":
|
|
|
|
p.opts.baseURL, p.opts.rootURL, p.opts.basePath, err = parseBaseURL(value)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
case "PORT":
|
|
|
|
port = value
|
|
|
|
case "LISTEN_ADDR":
|
|
|
|
p.opts.listenAddr = parseString(value, defaultListenAddr)
|
|
|
|
case "DATABASE_URL":
|
|
|
|
p.opts.databaseURL = parseString(value, defaultDatabaseURL)
|
2020-06-29 23:49:05 -04:00
|
|
|
case "DATABASE_URL_FILE":
|
|
|
|
p.opts.databaseURL = readSecretFile(value, defaultDatabaseURL)
|
2019-06-02 21:20:59 -04:00
|
|
|
case "DATABASE_MAX_CONNS":
|
|
|
|
p.opts.databaseMaxConns = parseInt(value, defaultDatabaseMaxConns)
|
|
|
|
case "DATABASE_MIN_CONNS":
|
|
|
|
p.opts.databaseMinConns = parseInt(value, defaultDatabaseMinConns)
|
|
|
|
case "RUN_MIGRATIONS":
|
|
|
|
p.opts.runMigrations = parseBool(value, defaultRunMigrations)
|
|
|
|
case "DISABLE_HSTS":
|
|
|
|
p.opts.hsts = !parseBool(value, defaultHSTS)
|
|
|
|
case "HTTPS":
|
|
|
|
p.opts.HTTPS = parseBool(value, defaultHTTPS)
|
|
|
|
case "DISABLE_SCHEDULER_SERVICE":
|
|
|
|
p.opts.schedulerService = !parseBool(value, defaultSchedulerService)
|
|
|
|
case "DISABLE_HTTP_SERVICE":
|
|
|
|
p.opts.httpService = !parseBool(value, defaultHTTPService)
|
|
|
|
case "CERT_FILE":
|
|
|
|
p.opts.certFile = parseString(value, defaultCertFile)
|
|
|
|
case "KEY_FILE":
|
|
|
|
p.opts.certKeyFile = parseString(value, defaultKeyFile)
|
|
|
|
case "CERT_DOMAIN":
|
|
|
|
p.opts.certDomain = parseString(value, defaultCertDomain)
|
|
|
|
case "CERT_CACHE":
|
|
|
|
p.opts.certCache = parseString(value, defaultCertCache)
|
2019-09-15 14:47:39 -04:00
|
|
|
case "CLEANUP_FREQUENCY_HOURS":
|
|
|
|
p.opts.cleanupFrequencyHours = parseInt(value, defaultCleanupFrequencyHours)
|
|
|
|
case "CLEANUP_ARCHIVE_READ_DAYS":
|
|
|
|
p.opts.cleanupArchiveReadDays = parseInt(value, defaultCleanupArchiveReadDays)
|
2020-09-12 23:04:06 -04:00
|
|
|
case "CLEANUP_ARCHIVE_UNREAD_DAYS":
|
|
|
|
p.opts.cleanupArchiveUnreadDays = parseInt(value, defaultCleanupArchiveUnreadDays)
|
2019-09-15 14:47:39 -04:00
|
|
|
case "CLEANUP_REMOVE_SESSIONS_DAYS":
|
|
|
|
p.opts.cleanupRemoveSessionsDays = parseInt(value, defaultCleanupRemoveSessionsDays)
|
2019-06-02 21:20:59 -04:00
|
|
|
case "WORKER_POOL_SIZE":
|
|
|
|
p.opts.workerPoolSize = parseInt(value, defaultWorkerPoolSize)
|
|
|
|
case "POLLING_FREQUENCY":
|
|
|
|
p.opts.pollingFrequency = parseInt(value, defaultPollingFrequency)
|
|
|
|
case "BATCH_SIZE":
|
|
|
|
p.opts.batchSize = parseInt(value, defaultBatchSize)
|
2020-05-25 17:06:56 -04:00
|
|
|
case "POLLING_SCHEDULER":
|
2020-05-25 17:59:15 -04:00
|
|
|
p.opts.pollingScheduler = strings.ToLower(parseString(value, defaultPollingScheduler))
|
|
|
|
case "SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL":
|
|
|
|
p.opts.schedulerEntryFrequencyMaxInterval = parseInt(value, defaultSchedulerEntryFrequencyMaxInterval)
|
|
|
|
case "SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL":
|
|
|
|
p.opts.schedulerEntryFrequencyMinInterval = parseInt(value, defaultSchedulerEntryFrequencyMinInterval)
|
2019-06-02 21:20:59 -04:00
|
|
|
case "PROXY_IMAGES":
|
|
|
|
p.opts.proxyImages = parseString(value, defaultProxyImages)
|
|
|
|
case "CREATE_ADMIN":
|
|
|
|
p.opts.createAdmin = parseBool(value, defaultCreateAdmin)
|
2020-06-29 23:49:05 -04:00
|
|
|
case "ADMIN_USERNAME":
|
|
|
|
p.opts.adminUsername = parseString(value, defaultAdminUsername)
|
|
|
|
case "ADMIN_USERNAME_FILE":
|
|
|
|
p.opts.adminUsername = readSecretFile(value, defaultAdminUsername)
|
|
|
|
case "ADMIN_PASSWORD":
|
|
|
|
p.opts.adminPassword = parseString(value, defaultAdminPassword)
|
|
|
|
case "ADMIN_PASSWORD_FILE":
|
|
|
|
p.opts.adminPassword = readSecretFile(value, defaultAdminPassword)
|
2019-06-02 21:20:59 -04:00
|
|
|
case "POCKET_CONSUMER_KEY":
|
|
|
|
p.opts.pocketConsumerKey = parseString(value, defaultPocketConsumerKey)
|
2020-06-29 23:49:05 -04:00
|
|
|
case "POCKET_CONSUMER_KEY_FILE":
|
|
|
|
p.opts.pocketConsumerKey = readSecretFile(value, defaultPocketConsumerKey)
|
2019-06-02 21:20:59 -04:00
|
|
|
case "OAUTH2_USER_CREATION":
|
|
|
|
p.opts.oauth2UserCreationAllowed = parseBool(value, defaultOAuth2UserCreation)
|
|
|
|
case "OAUTH2_CLIENT_ID":
|
|
|
|
p.opts.oauth2ClientID = parseString(value, defaultOAuth2ClientID)
|
2020-06-29 23:49:05 -04:00
|
|
|
case "OAUTH2_CLIENT_ID_FILE":
|
|
|
|
p.opts.oauth2ClientID = readSecretFile(value, defaultOAuth2ClientID)
|
2019-06-02 21:20:59 -04:00
|
|
|
case "OAUTH2_CLIENT_SECRET":
|
|
|
|
p.opts.oauth2ClientSecret = parseString(value, defaultOAuth2ClientSecret)
|
2020-06-29 23:49:05 -04:00
|
|
|
case "OAUTH2_CLIENT_SECRET_FILE":
|
|
|
|
p.opts.oauth2ClientSecret = readSecretFile(value, defaultOAuth2ClientSecret)
|
2019-06-02 21:20:59 -04:00
|
|
|
case "OAUTH2_REDIRECT_URL":
|
|
|
|
p.opts.oauth2RedirectURL = parseString(value, defaultOAuth2RedirectURL)
|
2020-03-07 21:45:19 -05:00
|
|
|
case "OAUTH2_OIDC_DISCOVERY_ENDPOINT":
|
|
|
|
p.opts.oauth2OidcDiscoveryEndpoint = parseString(value, defaultOAuth2OidcDiscoveryEndpoint)
|
2019-06-02 21:20:59 -04:00
|
|
|
case "OAUTH2_PROVIDER":
|
|
|
|
p.opts.oauth2Provider = parseString(value, defaultOAuth2Provider)
|
|
|
|
case "HTTP_CLIENT_TIMEOUT":
|
|
|
|
p.opts.httpClientTimeout = parseInt(value, defaultHTTPClientTimeout)
|
|
|
|
case "HTTP_CLIENT_MAX_BODY_SIZE":
|
|
|
|
p.opts.httpClientMaxBodySize = int64(parseInt(value, defaultHTTPClientMaxBodySize) * 1024 * 1024)
|
2020-09-10 02:28:54 -04:00
|
|
|
case "HTTP_CLIENT_PROXY":
|
|
|
|
p.opts.httpClientProxy = parseString(value, defaultHTTPClientProxy)
|
2020-01-29 05:45:59 -05:00
|
|
|
case "AUTH_PROXY_HEADER":
|
|
|
|
p.opts.authProxyHeader = parseString(value, defaultAuthProxyHeader)
|
|
|
|
case "AUTH_PROXY_USER_CREATION":
|
|
|
|
p.opts.authProxyUserCreation = parseBool(value, defaultAuthProxyUserCreation)
|
2020-09-12 21:31:45 -04:00
|
|
|
case "MAINTENANCE_MODE":
|
|
|
|
p.opts.maintenanceMode = parseBool(value, defaultMaintenanceMode)
|
|
|
|
case "MAINTENANCE_MESSAGE":
|
|
|
|
p.opts.maintenanceMessage = parseString(value, defaultMaintenanceMessage)
|
2019-06-02 21:20:59 -04:00
|
|
|
}
|
|
|
|
}
|
2019-06-01 21:18:09 -04:00
|
|
|
|
2019-06-02 21:20:59 -04:00
|
|
|
if port != "" {
|
|
|
|
p.opts.listenAddr = ":" + port
|
|
|
|
}
|
|
|
|
return nil
|
2019-06-01 21:18:09 -04:00
|
|
|
}
|
|
|
|
|
2019-06-02 21:20:59 -04:00
|
|
|
func parseBaseURL(value string) (string, string, string, error) {
|
|
|
|
if value == "" {
|
|
|
|
return defaultBaseURL, defaultRootURL, "", nil
|
2019-06-01 21:18:09 -04:00
|
|
|
}
|
|
|
|
|
2019-06-02 21:20:59 -04:00
|
|
|
if value[len(value)-1:] == "/" {
|
|
|
|
value = value[:len(value)-1]
|
2019-06-01 21:18:09 -04:00
|
|
|
}
|
|
|
|
|
2019-06-02 21:20:59 -04:00
|
|
|
url, err := url_parser.Parse(value)
|
2019-06-01 21:18:09 -04:00
|
|
|
if err != nil {
|
|
|
|
return "", "", "", fmt.Errorf("Invalid BASE_URL: %v", err)
|
|
|
|
}
|
|
|
|
|
2019-06-02 21:20:59 -04:00
|
|
|
scheme := strings.ToLower(url.Scheme)
|
2019-06-01 21:18:09 -04:00
|
|
|
if scheme != "https" && scheme != "http" {
|
|
|
|
return "", "", "", errors.New("Invalid BASE_URL: scheme must be http or https")
|
|
|
|
}
|
|
|
|
|
2019-06-02 21:20:59 -04:00
|
|
|
basePath := url.Path
|
|
|
|
url.Path = ""
|
|
|
|
return value, url.String(), basePath, nil
|
2019-06-01 21:18:09 -04:00
|
|
|
}
|
|
|
|
|
2019-06-02 21:20:59 -04:00
|
|
|
func parseBool(value string, fallback bool) bool {
|
|
|
|
if value == "" {
|
|
|
|
return fallback
|
2019-06-01 21:18:09 -04:00
|
|
|
}
|
|
|
|
|
2019-06-02 21:20:59 -04:00
|
|
|
value = strings.ToLower(value)
|
2019-06-01 21:18:09 -04:00
|
|
|
if value == "1" || value == "yes" || value == "true" || value == "on" {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2019-06-02 21:20:59 -04:00
|
|
|
return false
|
2019-06-01 21:18:09 -04:00
|
|
|
}
|
|
|
|
|
2019-06-02 21:20:59 -04:00
|
|
|
func parseInt(value string, fallback int) int {
|
2019-06-01 21:18:09 -04:00
|
|
|
if value == "" {
|
|
|
|
return fallback
|
|
|
|
}
|
|
|
|
|
|
|
|
v, err := strconv.Atoi(value)
|
|
|
|
if err != nil {
|
|
|
|
return fallback
|
|
|
|
}
|
|
|
|
|
|
|
|
return v
|
|
|
|
}
|
2019-06-02 21:20:59 -04:00
|
|
|
|
|
|
|
func parseString(value string, fallback string) string {
|
|
|
|
if value == "" {
|
|
|
|
return fallback
|
|
|
|
}
|
|
|
|
return value
|
|
|
|
}
|
2020-06-29 23:49:05 -04:00
|
|
|
|
|
|
|
func readSecretFile(filename, fallback string) string {
|
|
|
|
data, err := ioutil.ReadFile(filename)
|
|
|
|
if err != nil {
|
|
|
|
return fallback
|
|
|
|
}
|
|
|
|
|
|
|
|
value := string(bytes.TrimSpace(data))
|
|
|
|
if value == "" {
|
|
|
|
return fallback
|
|
|
|
}
|
|
|
|
|
|
|
|
return value
|
|
|
|
}
|