2023-06-19 14:42:47 -07:00
|
|
|
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
2019-06-01 18:18:09 -07:00
|
|
|
|
2023-08-10 19:46:45 -07:00
|
|
|
package config // import "miniflux.app/v2/internal/config"
|
2019-06-01 18:18:09 -07:00
|
|
|
|
|
|
|
import (
|
2019-06-02 18:20:59 -07:00
|
|
|
"bufio"
|
2020-06-29 20:49:05 -07:00
|
|
|
"bytes"
|
2022-10-15 08:17:17 +02:00
|
|
|
"crypto/rand"
|
2019-06-01 18:18:09 -07:00
|
|
|
"fmt"
|
2019-06-02 18:20:59 -07:00
|
|
|
"io"
|
2023-09-24 16:32:09 -07:00
|
|
|
"net/url"
|
2019-06-01 18:18:09 -07:00
|
|
|
"os"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
2025-08-18 23:10:18 +03:00
|
|
|
"time"
|
2019-06-01 18:18:09 -07:00
|
|
|
)
|
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
type configParser struct {
|
|
|
|
options *configOptions
|
2019-06-02 18:20:59 -07:00
|
|
|
}
|
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
func NewConfigParser() *configParser {
|
|
|
|
return &configParser{
|
|
|
|
options: NewConfigOptions(),
|
2019-06-02 18:20:59 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
func (cp *configParser) ParseEnvironmentVariables() (*configOptions, error) {
|
|
|
|
if err := cp.parseLines(os.Environ()); err != nil {
|
2019-06-01 18:18:09 -07:00
|
|
|
return nil, err
|
|
|
|
}
|
2025-09-14 10:51:04 -07:00
|
|
|
|
|
|
|
return cp.options, nil
|
2019-06-02 18:20:59 -07:00
|
|
|
}
|
2019-06-01 18:18:09 -07:00
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
func (cp *configParser) ParseFile(filename string) (*configOptions, error) {
|
2019-06-02 18:20:59 -07:00
|
|
|
fp, err := os.Open(filename)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer fp.Close()
|
2019-06-01 18:18:09 -07:00
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
if err := cp.parseLines(parseFileContent(fp)); err != nil {
|
2019-06-02 18:20:59 -07:00
|
|
|
return nil, err
|
|
|
|
}
|
2025-09-14 10:51:04 -07:00
|
|
|
|
|
|
|
return cp.options, nil
|
2019-06-02 18:20:59 -07:00
|
|
|
}
|
2019-06-01 18:18:09 -07:00
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
func (cp *configParser) postParsing() error {
|
|
|
|
// Parse basePath and rootURL based on BASE_URL
|
|
|
|
baseURL := cp.options.options["BASE_URL"].ParsedStringValue
|
|
|
|
baseURL = strings.TrimSuffix(baseURL, "/")
|
|
|
|
|
|
|
|
parsedURL, err := url.Parse(baseURL)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("invalid BASE_URL: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
scheme := strings.ToLower(parsedURL.Scheme)
|
|
|
|
if scheme != "https" && scheme != "http" {
|
|
|
|
return fmt.Errorf("BASE_URL scheme must be http or https")
|
|
|
|
}
|
|
|
|
|
|
|
|
cp.options.options["BASE_URL"].ParsedStringValue = baseURL
|
|
|
|
cp.options.basePath = parsedURL.Path
|
|
|
|
|
|
|
|
parsedURL.Path = ""
|
|
|
|
cp.options.rootURL = parsedURL.String()
|
|
|
|
|
|
|
|
// Parse YouTube embed domain based on YOUTUBE_EMBED_URL_OVERRIDE
|
|
|
|
youTubeEmbedURLOverride := cp.options.options["YOUTUBE_EMBED_URL_OVERRIDE"].ParsedStringValue
|
|
|
|
if youTubeEmbedURLOverride != "" {
|
|
|
|
parsedYouTubeEmbedURL, err := url.Parse(youTubeEmbedURLOverride)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("invalid YOUTUBE_EMBED_URL_OVERRIDE: %v", err)
|
2019-06-02 18:20:59 -07:00
|
|
|
}
|
2025-09-14 10:51:04 -07:00
|
|
|
cp.options.youTubeEmbedDomain = parsedYouTubeEmbedURL.Hostname()
|
2019-06-02 18:20:59 -07:00
|
|
|
}
|
2019-06-01 18:18:09 -07:00
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
// Generate a media proxy private key if not set
|
|
|
|
if len(cp.options.options["MEDIA_PROXY_PRIVATE_KEY"].ParsedBytesValue) == 0 {
|
|
|
|
randomKey := make([]byte, 16)
|
|
|
|
rand.Read(randomKey)
|
|
|
|
cp.options.options["MEDIA_PROXY_PRIVATE_KEY"].ParsedBytesValue = randomKey
|
|
|
|
}
|
2019-06-02 18:20:59 -07:00
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
// Override LISTEN_ADDR with PORT if set (for compatibility reasons)
|
|
|
|
if cp.options.Port() != "" {
|
|
|
|
cp.options.options["LISTEN_ADDR"].ParsedStringList = []string{":" + cp.options.Port()}
|
|
|
|
cp.options.options["LISTEN_ADDR"].RawValue = ":" + cp.options.Port()
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cp *configParser) parseLines(lines []string) error {
|
2025-07-09 17:06:02 +02:00
|
|
|
for lineNum, line := range lines {
|
|
|
|
key, value, ok := strings.Cut(line, "=")
|
|
|
|
if !ok {
|
2025-09-14 10:51:04 -07:00
|
|
|
return fmt.Errorf("unable to parse configuration, invalid format on line %d", lineNum)
|
2025-07-09 17:06:02 +02:00
|
|
|
}
|
2019-06-02 18:20:59 -07:00
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
key, value = strings.TrimSpace(key), strings.TrimSpace(value)
|
|
|
|
if err := cp.parseLine(key, value); err != nil {
|
|
|
|
return err
|
2019-06-02 18:20:59 -07:00
|
|
|
}
|
|
|
|
}
|
2019-06-01 18:18:09 -07:00
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
if err := cp.postParsing(); err != nil {
|
|
|
|
return err
|
2019-06-02 18:20:59 -07:00
|
|
|
}
|
2025-06-13 20:44:47 -07:00
|
|
|
|
2019-06-02 18:20:59 -07:00
|
|
|
return nil
|
2019-06-01 18:18:09 -07:00
|
|
|
}
|
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
func (cp *configParser) parseLine(key, value string) error {
|
|
|
|
field, exists := cp.options.options[key]
|
|
|
|
if !exists {
|
|
|
|
// Ignore unknown configuration keys to avoid parsing unrelated environment variables.
|
|
|
|
return nil
|
2019-06-01 18:18:09 -07:00
|
|
|
}
|
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
// Validate the option if a validator is provided
|
|
|
|
if field.Validator != nil {
|
|
|
|
if err := field.Validator(value); err != nil {
|
|
|
|
return fmt.Errorf("invalid value for key %s: %v", key, err)
|
|
|
|
}
|
2019-06-01 18:18:09 -07:00
|
|
|
}
|
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
// Convert the raw value based on its type
|
|
|
|
switch field.ValueType {
|
|
|
|
case stringType:
|
|
|
|
field.ParsedStringValue = parseStringValue(value, field.ParsedStringValue)
|
|
|
|
field.RawValue = value
|
|
|
|
case stringListType:
|
|
|
|
field.ParsedStringList = parseStringListValue(value, field.ParsedStringList)
|
|
|
|
field.RawValue = value
|
|
|
|
case boolType:
|
|
|
|
parsedValue, err := parseBoolValue(value, field.ParsedBoolValue)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("invalid boolean value for key %s: %v", key, err)
|
|
|
|
}
|
|
|
|
field.ParsedBoolValue = parsedValue
|
|
|
|
field.RawValue = value
|
|
|
|
case intType:
|
|
|
|
field.ParsedIntValue = parseIntValue(value, field.ParsedIntValue)
|
|
|
|
field.RawValue = value
|
|
|
|
case int64Type:
|
|
|
|
field.ParsedInt64Value = ParsedInt64Value(value, field.ParsedInt64Value)
|
|
|
|
field.RawValue = value
|
|
|
|
case secondType:
|
|
|
|
field.ParsedDuration = parseDurationValue(value, time.Second, field.ParsedDuration)
|
|
|
|
field.RawValue = value
|
|
|
|
case minuteType:
|
|
|
|
field.ParsedDuration = parseDurationValue(value, time.Minute, field.ParsedDuration)
|
|
|
|
field.RawValue = value
|
|
|
|
case hourType:
|
|
|
|
field.ParsedDuration = parseDurationValue(value, time.Hour, field.ParsedDuration)
|
|
|
|
field.RawValue = value
|
|
|
|
case dayType:
|
|
|
|
field.ParsedDuration = parseDurationValue(value, time.Hour*24, field.ParsedDuration)
|
|
|
|
field.RawValue = value
|
|
|
|
case urlType:
|
|
|
|
parsedURL, err := parseURLValue(value, field.ParsedURLValue)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("invalid URL for key %s: %v", key, err)
|
|
|
|
}
|
|
|
|
field.ParsedURLValue = parsedURL
|
|
|
|
field.RawValue = value
|
|
|
|
case secretFileType:
|
|
|
|
secretValue, err := readSecretFileValue(value)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error reading secret file for key %s: %v", key, err)
|
|
|
|
}
|
|
|
|
if field.TargetKey != "" {
|
|
|
|
if targetField, ok := cp.options.options[field.TargetKey]; ok {
|
|
|
|
targetField.ParsedStringValue = secretValue
|
|
|
|
targetField.RawValue = secretValue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
field.RawValue = value
|
|
|
|
case bytesType:
|
|
|
|
if value != "" {
|
|
|
|
field.ParsedBytesValue = []byte(value)
|
|
|
|
field.RawValue = value
|
|
|
|
}
|
2019-06-01 18:18:09 -07:00
|
|
|
}
|
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
return nil
|
2019-06-01 18:18:09 -07:00
|
|
|
}
|
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
func parseStringValue(value string, fallback string) string {
|
2019-06-02 18:20:59 -07:00
|
|
|
if value == "" {
|
|
|
|
return fallback
|
2019-06-01 18:18:09 -07:00
|
|
|
}
|
2025-09-14 10:51:04 -07:00
|
|
|
return value
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseBoolValue(value string, fallback bool) (bool, error) {
|
|
|
|
if value == "" {
|
|
|
|
return fallback, nil
|
|
|
|
}
|
2019-06-01 18:18:09 -07:00
|
|
|
|
2019-06-02 18:20:59 -07:00
|
|
|
value = strings.ToLower(value)
|
2019-06-01 18:18:09 -07:00
|
|
|
if value == "1" || value == "yes" || value == "true" || value == "on" {
|
2025-09-14 10:51:04 -07:00
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
if value == "0" || value == "no" || value == "false" || value == "off" {
|
|
|
|
return false, nil
|
2019-06-01 18:18:09 -07:00
|
|
|
}
|
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
return false, fmt.Errorf("invalid boolean value: %q", value)
|
2019-06-01 18:18:09 -07:00
|
|
|
}
|
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
func parseIntValue(value string, fallback int) int {
|
2019-06-01 18:18:09 -07:00
|
|
|
if value == "" {
|
|
|
|
return fallback
|
|
|
|
}
|
|
|
|
|
|
|
|
v, err := strconv.Atoi(value)
|
|
|
|
if err != nil {
|
|
|
|
return fallback
|
|
|
|
}
|
|
|
|
|
|
|
|
return v
|
|
|
|
}
|
2019-06-02 18:20:59 -07:00
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
func ParsedInt64Value(value string, fallback int64) int64 {
|
2019-06-02 18:20:59 -07:00
|
|
|
if value == "" {
|
|
|
|
return fallback
|
|
|
|
}
|
2025-09-14 10:51:04 -07:00
|
|
|
|
|
|
|
v, err := strconv.ParseInt(value, 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
return fallback
|
|
|
|
}
|
|
|
|
|
|
|
|
return v
|
2019-06-02 18:20:59 -07:00
|
|
|
}
|
2020-06-29 20:49:05 -07:00
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
func parseStringListValue(value string, fallback []string) []string {
|
2020-09-27 16:01:06 -07:00
|
|
|
if value == "" {
|
|
|
|
return fallback
|
|
|
|
}
|
|
|
|
|
|
|
|
var strList []string
|
2025-07-09 17:06:02 +02:00
|
|
|
present := make(map[string]bool)
|
2023-04-02 18:24:29 -07:00
|
|
|
|
2025-07-09 17:06:02 +02:00
|
|
|
for item := range strings.SplitSeq(value, ",") {
|
|
|
|
if itemValue := strings.TrimSpace(item); itemValue != "" {
|
|
|
|
if !present[itemValue] {
|
|
|
|
present[itemValue] = true
|
|
|
|
strList = append(strList, itemValue)
|
|
|
|
}
|
2023-04-02 18:24:29 -07:00
|
|
|
}
|
2020-09-27 16:01:06 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return strList
|
|
|
|
}
|
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
func parseDurationValue(value string, unit time.Duration, fallback time.Duration) time.Duration {
|
2022-10-15 08:17:17 +02:00
|
|
|
if value == "" {
|
|
|
|
return fallback
|
|
|
|
}
|
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
v, err := strconv.Atoi(value)
|
|
|
|
if err != nil {
|
|
|
|
return fallback
|
|
|
|
}
|
|
|
|
|
|
|
|
return time.Duration(v) * unit
|
2022-10-15 08:17:17 +02:00
|
|
|
}
|
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
func parseURLValue(value string, fallback *url.URL) (*url.URL, error) {
|
2025-08-18 23:10:18 +03:00
|
|
|
if value == "" {
|
2025-09-14 10:51:04 -07:00
|
|
|
return fallback, nil
|
2025-08-18 23:10:18 +03:00
|
|
|
}
|
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
parsedURL, err := url.Parse(value)
|
2025-08-18 23:10:18 +03:00
|
|
|
if err != nil {
|
2025-09-14 10:51:04 -07:00
|
|
|
return fallback, err
|
2025-08-18 23:10:18 +03:00
|
|
|
}
|
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
return parsedURL, nil
|
2025-08-18 23:10:18 +03:00
|
|
|
}
|
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
func readSecretFileValue(filename string) (string, error) {
|
2021-02-16 21:19:03 -08:00
|
|
|
data, err := os.ReadFile(filename)
|
2020-06-29 20:49:05 -07:00
|
|
|
if err != nil {
|
2025-09-14 10:51:04 -07:00
|
|
|
return "", err
|
2020-06-29 20:49:05 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
value := string(bytes.TrimSpace(data))
|
|
|
|
if value == "" {
|
2025-09-14 10:51:04 -07:00
|
|
|
return "", fmt.Errorf("secret file is empty")
|
2020-06-29 20:49:05 -07:00
|
|
|
}
|
|
|
|
|
2025-09-14 10:51:04 -07:00
|
|
|
return value, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseFileContent(r io.Reader) (lines []string) {
|
|
|
|
scanner := bufio.NewScanner(r)
|
|
|
|
for scanner.Scan() {
|
|
|
|
line := strings.TrimSpace(scanner.Text())
|
|
|
|
if !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 {
|
|
|
|
lines = append(lines, line)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return lines
|
2020-06-29 20:49:05 -07:00
|
|
|
}
|