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 5package config // import "miniflux.app/config" 6 7import ( 8 "bufio" 9 "bytes" 10 "errors" 11 "fmt" 12 "io" 13 url_parser "net/url" 14 "os" 15 "strconv" 16 "strings" 17) 18 19// Parser handles configuration parsing. 20type Parser struct { 21 opts *Options 22} 23 24// NewParser returns a new Parser. 25func NewParser() *Parser { 26 return &Parser{ 27 opts: NewOptions(), 28 } 29} 30 31// ParseEnvironmentVariables loads configuration values from environment variables. 32func (p *Parser) ParseEnvironmentVariables() (*Options, error) { 33 err := p.parseLines(os.Environ()) 34 if err != nil { 35 return nil, err 36 } 37 return p.opts, nil 38} 39 40// ParseFile loads configuration values from a local file. 41func (p *Parser) ParseFile(filename string) (*Options, error) { 42 fp, err := os.Open(filename) 43 if err != nil { 44 return nil, err 45 } 46 defer fp.Close() 47 48 err = p.parseLines(p.parseFileContent(fp)) 49 if err != nil { 50 return nil, err 51 } 52 return p.opts, nil 53} 54 55func (p *Parser) parseFileContent(r io.Reader) (lines []string) { 56 scanner := bufio.NewScanner(r) 57 for scanner.Scan() { 58 line := strings.TrimSpace(scanner.Text()) 59 if len(line) > 0 && !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 { 60 lines = append(lines, line) 61 } 62 } 63 return lines 64} 65 66func (p *Parser) parseLines(lines []string) (err error) { 67 var port string 68 69 for _, line := range lines { 70 fields := strings.SplitN(line, "=", 2) 71 key := strings.TrimSpace(fields[0]) 72 value := strings.TrimSpace(fields[1]) 73 74 switch key { 75 case "LOG_DATE_TIME": 76 p.opts.logDateTime = parseBool(value, defaultLogDateTime) 77 case "DEBUG": 78 p.opts.debug = parseBool(value, defaultDebug) 79 case "SERVER_TIMING_HEADER": 80 p.opts.serverTimingHeader = parseBool(value, defaultTiming) 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 = parseString(value, 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 "RUN_MIGRATIONS": 101 p.opts.runMigrations = parseBool(value, defaultRunMigrations) 102 case "DISABLE_HSTS": 103 p.opts.hsts = !parseBool(value, defaultHSTS) 104 case "HTTPS": 105 p.opts.HTTPS = parseBool(value, defaultHTTPS) 106 case "DISABLE_SCHEDULER_SERVICE": 107 p.opts.schedulerService = !parseBool(value, defaultSchedulerService) 108 case "DISABLE_HTTP_SERVICE": 109 p.opts.httpService = !parseBool(value, defaultHTTPService) 110 case "CERT_FILE": 111 p.opts.certFile = parseString(value, defaultCertFile) 112 case "KEY_FILE": 113 p.opts.certKeyFile = parseString(value, defaultKeyFile) 114 case "CERT_DOMAIN": 115 p.opts.certDomain = parseString(value, defaultCertDomain) 116 case "CLEANUP_FREQUENCY_HOURS": 117 p.opts.cleanupFrequencyHours = parseInt(value, defaultCleanupFrequencyHours) 118 case "CLEANUP_ARCHIVE_READ_DAYS": 119 p.opts.cleanupArchiveReadDays = parseInt(value, defaultCleanupArchiveReadDays) 120 case "CLEANUP_ARCHIVE_UNREAD_DAYS": 121 p.opts.cleanupArchiveUnreadDays = parseInt(value, defaultCleanupArchiveUnreadDays) 122 case "CLEANUP_ARCHIVE_BATCH_SIZE": 123 p.opts.cleanupArchiveBatchSize = parseInt(value, defaultCleanupArchiveBatchSize) 124 case "CLEANUP_REMOVE_SESSIONS_DAYS": 125 p.opts.cleanupRemoveSessionsDays = parseInt(value, defaultCleanupRemoveSessionsDays) 126 case "WORKER_POOL_SIZE": 127 p.opts.workerPoolSize = parseInt(value, defaultWorkerPoolSize) 128 case "POLLING_FREQUENCY": 129 p.opts.pollingFrequency = parseInt(value, defaultPollingFrequency) 130 case "BATCH_SIZE": 131 p.opts.batchSize = parseInt(value, defaultBatchSize) 132 case "POLLING_SCHEDULER": 133 p.opts.pollingScheduler = strings.ToLower(parseString(value, defaultPollingScheduler)) 134 case "SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL": 135 p.opts.schedulerEntryFrequencyMaxInterval = parseInt(value, defaultSchedulerEntryFrequencyMaxInterval) 136 case "SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL": 137 p.opts.schedulerEntryFrequencyMinInterval = parseInt(value, defaultSchedulerEntryFrequencyMinInterval) 138 case "POLLING_PARSING_ERROR_LIMIT": 139 p.opts.pollingParsingErrorLimit = parseInt(value, defaultPollingParsingErrorLimit) 140 case "PROXY_IMAGES": 141 p.opts.proxyImages = parseString(value, defaultProxyImages) 142 case "CREATE_ADMIN": 143 p.opts.createAdmin = parseBool(value, defaultCreateAdmin) 144 case "ADMIN_USERNAME": 145 p.opts.adminUsername = parseString(value, defaultAdminUsername) 146 case "ADMIN_USERNAME_FILE": 147 p.opts.adminUsername = readSecretFile(value, defaultAdminUsername) 148 case "ADMIN_PASSWORD": 149 p.opts.adminPassword = parseString(value, defaultAdminPassword) 150 case "ADMIN_PASSWORD_FILE": 151 p.opts.adminPassword = readSecretFile(value, defaultAdminPassword) 152 case "POCKET_CONSUMER_KEY": 153 p.opts.pocketConsumerKey = parseString(value, defaultPocketConsumerKey) 154 case "POCKET_CONSUMER_KEY_FILE": 155 p.opts.pocketConsumerKey = readSecretFile(value, defaultPocketConsumerKey) 156 case "OAUTH2_USER_CREATION": 157 p.opts.oauth2UserCreationAllowed = parseBool(value, defaultOAuth2UserCreation) 158 case "OAUTH2_CLIENT_ID": 159 p.opts.oauth2ClientID = parseString(value, defaultOAuth2ClientID) 160 case "OAUTH2_CLIENT_ID_FILE": 161 p.opts.oauth2ClientID = readSecretFile(value, defaultOAuth2ClientID) 162 case "OAUTH2_CLIENT_SECRET": 163 p.opts.oauth2ClientSecret = parseString(value, defaultOAuth2ClientSecret) 164 case "OAUTH2_CLIENT_SECRET_FILE": 165 p.opts.oauth2ClientSecret = readSecretFile(value, defaultOAuth2ClientSecret) 166 case "OAUTH2_REDIRECT_URL": 167 p.opts.oauth2RedirectURL = parseString(value, defaultOAuth2RedirectURL) 168 case "OAUTH2_OIDC_DISCOVERY_ENDPOINT": 169 p.opts.oauth2OidcDiscoveryEndpoint = parseString(value, defaultOAuth2OidcDiscoveryEndpoint) 170 case "OAUTH2_PROVIDER": 171 p.opts.oauth2Provider = parseString(value, defaultOAuth2Provider) 172 case "HTTP_CLIENT_TIMEOUT": 173 p.opts.httpClientTimeout = parseInt(value, defaultHTTPClientTimeout) 174 case "HTTP_CLIENT_MAX_BODY_SIZE": 175 p.opts.httpClientMaxBodySize = int64(parseInt(value, defaultHTTPClientMaxBodySize) * 1024 * 1024) 176 case "HTTP_CLIENT_PROXY": 177 p.opts.httpClientProxy = parseString(value, defaultHTTPClientProxy) 178 case "HTTP_CLIENT_USER_AGENT": 179 p.opts.httpClientUserAgent = parseString(value, defaultHTTPClientUserAgent) 180 case "AUTH_PROXY_HEADER": 181 p.opts.authProxyHeader = parseString(value, defaultAuthProxyHeader) 182 case "AUTH_PROXY_USER_CREATION": 183 p.opts.authProxyUserCreation = parseBool(value, defaultAuthProxyUserCreation) 184 case "MAINTENANCE_MODE": 185 p.opts.maintenanceMode = parseBool(value, defaultMaintenanceMode) 186 case "MAINTENANCE_MESSAGE": 187 p.opts.maintenanceMessage = parseString(value, defaultMaintenanceMessage) 188 case "METRICS_COLLECTOR": 189 p.opts.metricsCollector = parseBool(value, defaultMetricsCollector) 190 case "METRICS_REFRESH_INTERVAL": 191 p.opts.metricsRefreshInterval = parseInt(value, defaultMetricsRefreshInterval) 192 case "METRICS_ALLOWED_NETWORKS": 193 p.opts.metricsAllowedNetworks = parseStringList(value, []string{defaultMetricsAllowedNetworks}) 194 case "FETCH_YOUTUBE_WATCH_TIME": 195 p.opts.fetchYouTubeWatchTime = parseBool(value, defaultFetchYouTubeWatchTime) 196 case "WATCHDOG": 197 p.opts.watchdog = parseBool(value, defaultWatchdog) 198 case "INVIDIOUS_INSTANCE": 199 p.opts.invidiousInstance = parseString(value, defaultInvidiousInstance) 200 } 201 } 202 203 if port != "" { 204 p.opts.listenAddr = ":" + port 205 } 206 return nil 207} 208 209func parseBaseURL(value string) (string, string, string, error) { 210 if value == "" { 211 return defaultBaseURL, defaultRootURL, "", nil 212 } 213 214 if value[len(value)-1:] == "/" { 215 value = value[:len(value)-1] 216 } 217 218 url, err := url_parser.Parse(value) 219 if err != nil { 220 return "", "", "", fmt.Errorf("config: invalid BASE_URL: %w", err) 221 } 222 223 scheme := strings.ToLower(url.Scheme) 224 if scheme != "https" && scheme != "http" { 225 return "", "", "", errors.New("config: invalid BASE_URL: scheme must be http or https") 226 } 227 228 basePath := url.Path 229 url.Path = "" 230 return value, url.String(), basePath, nil 231} 232 233func parseBool(value string, fallback bool) bool { 234 if value == "" { 235 return fallback 236 } 237 238 value = strings.ToLower(value) 239 if value == "1" || value == "yes" || value == "true" || value == "on" { 240 return true 241 } 242 243 return false 244} 245 246func parseInt(value string, fallback int) int { 247 if value == "" { 248 return fallback 249 } 250 251 v, err := strconv.Atoi(value) 252 if err != nil { 253 return fallback 254 } 255 256 return v 257} 258 259func parseString(value string, fallback string) string { 260 if value == "" { 261 return fallback 262 } 263 return value 264} 265 266func parseStringList(value string, fallback []string) []string { 267 if value == "" { 268 return fallback 269 } 270 271 var strList []string 272 items := strings.Split(value, ",") 273 for _, item := range items { 274 strList = append(strList, strings.TrimSpace(item)) 275 } 276 277 return strList 278} 279 280func readSecretFile(filename, fallback string) string { 281 data, err := os.ReadFile(filename) 282 if err != nil { 283 return fallback 284 } 285 286 value := string(bytes.TrimSpace(data)) 287 if value == "" { 288 return fallback 289 } 290 291 return value 292} 293