Skip to content

Conversation

@jvoisin
Copy link
Collaborator

@jvoisin jvoisin commented Jul 6, 2025

Some users aren't using those, it thus makes sense to provide a way to disable them, as they expose quite a lot of sensitive-ish features.

Copy link
Member

@fguillot fguillot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update the man page with the new config options.

@jvoisin
Copy link
Collaborator Author

jvoisin commented Jul 8, 2025

Good call

Copy link
Member

@fguillot fguillot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You also need to update this switch-case, and add unit tests where applicable:

switch key {
case "LOG_FILE":
p.opts.logFile = parseString(value, defaultLogFile)
case "LOG_DATE_TIME":
p.opts.logDateTime = parseBool(value, defaultLogDateTime)
case "LOG_LEVEL":
parsedValue := parseString(value, defaultLogLevel)
if parsedValue == "debug" || parsedValue == "info" || parsedValue == "warning" || parsedValue == "error" {
p.opts.logLevel = parsedValue
}
case "LOG_FORMAT":
parsedValue := parseString(value, defaultLogFormat)
if parsedValue == "json" || parsedValue == "text" {
p.opts.logFormat = parsedValue
}
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 = parseStringList(value, []string{defaultListenAddr})
case "DATABASE_URL":
p.opts.databaseURL = parseString(value, defaultDatabaseURL)
case "DATABASE_URL_FILE":
p.opts.databaseURL = readSecretFile(value, defaultDatabaseURL)
case "DATABASE_MAX_CONNS":
p.opts.databaseMaxConns = parseInt(value, defaultDatabaseMaxConns)
case "DATABASE_MIN_CONNS":
p.opts.databaseMinConns = parseInt(value, defaultDatabaseMinConns)
case "DATABASE_CONNECTION_LIFETIME":
p.opts.databaseConnectionLifetime = parseInt(value, defaultDatabaseConnectionLifetime)
case "FILTER_ENTRY_MAX_AGE_DAYS":
p.opts.filterEntryMaxAgeDays = parseInt(value, defaultFilterEntryMaxAgeDays)
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 "CLEANUP_FREQUENCY_HOURS":
p.opts.cleanupFrequencyHours = parseInt(value, defaultCleanupFrequencyHours)
case "CLEANUP_ARCHIVE_READ_DAYS":
p.opts.cleanupArchiveReadDays = parseInt(value, defaultCleanupArchiveReadDays)
case "CLEANUP_ARCHIVE_UNREAD_DAYS":
p.opts.cleanupArchiveUnreadDays = parseInt(value, defaultCleanupArchiveUnreadDays)
case "CLEANUP_ARCHIVE_BATCH_SIZE":
p.opts.cleanupArchiveBatchSize = parseInt(value, defaultCleanupArchiveBatchSize)
case "CLEANUP_REMOVE_SESSIONS_DAYS":
p.opts.cleanupRemoveSessionsDays = parseInt(value, defaultCleanupRemoveSessionsDays)
case "WORKER_POOL_SIZE":
p.opts.workerPoolSize = parseInt(value, defaultWorkerPoolSize)
case "POLLING_FREQUENCY":
p.opts.pollingFrequency = parseInt(value, defaultPollingFrequency)
case "FORCE_REFRESH_INTERVAL":
p.opts.forceRefreshInterval = parseInt(value, defaultForceRefreshInterval)
case "BATCH_SIZE":
p.opts.batchSize = parseInt(value, defaultBatchSize)
case "POLLING_SCHEDULER":
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)
case "SCHEDULER_ENTRY_FREQUENCY_FACTOR":
p.opts.schedulerEntryFrequencyFactor = parseInt(value, defaultSchedulerEntryFrequencyFactor)
case "SCHEDULER_ROUND_ROBIN_MIN_INTERVAL":
p.opts.schedulerRoundRobinMinInterval = parseInt(value, defaultSchedulerRoundRobinMinInterval)
case "SCHEDULER_ROUND_ROBIN_MAX_INTERVAL":
p.opts.schedulerRoundRobinMaxInterval = parseInt(value, defaultSchedulerRoundRobinMaxInterval)
case "POLLING_PARSING_ERROR_LIMIT":
p.opts.pollingParsingErrorLimit = parseInt(value, defaultPollingParsingErrorLimit)
case "MEDIA_PROXY_HTTP_CLIENT_TIMEOUT":
p.opts.mediaProxyHTTPClientTimeout = parseInt(value, defaultMediaProxyHTTPClientTimeout)
case "MEDIA_PROXY_MODE":
p.opts.mediaProxyMode = parseString(value, defaultMediaProxyMode)
case "MEDIA_PROXY_RESOURCE_TYPES":
p.opts.mediaProxyResourceTypes = parseStringList(value, []string{defaultMediaResourceTypes})
case "MEDIA_PROXY_PRIVATE_KEY":
randomKey := make([]byte, 16)
if _, err := rand.Read(randomKey); err != nil {
return fmt.Errorf("config: unable to generate random key: %w", err)
}
p.opts.mediaProxyPrivateKey = parseBytes(value, randomKey)
case "MEDIA_PROXY_CUSTOM_URL":
p.opts.mediaProxyCustomURL = parseString(value, defaultMediaProxyURL)
case "CREATE_ADMIN":
p.opts.createAdmin = parseBool(value, defaultCreateAdmin)
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)
case "OAUTH2_USER_CREATION":
p.opts.oauth2UserCreationAllowed = parseBool(value, defaultOAuth2UserCreation)
case "OAUTH2_CLIENT_ID":
p.opts.oauth2ClientID = parseString(value, defaultOAuth2ClientID)
case "OAUTH2_CLIENT_ID_FILE":
p.opts.oauth2ClientID = readSecretFile(value, defaultOAuth2ClientID)
case "OAUTH2_CLIENT_SECRET":
p.opts.oauth2ClientSecret = parseString(value, defaultOAuth2ClientSecret)
case "OAUTH2_CLIENT_SECRET_FILE":
p.opts.oauth2ClientSecret = readSecretFile(value, defaultOAuth2ClientSecret)
case "OAUTH2_REDIRECT_URL":
p.opts.oauth2RedirectURL = parseString(value, defaultOAuth2RedirectURL)
case "OAUTH2_OIDC_DISCOVERY_ENDPOINT":
p.opts.oidcDiscoveryEndpoint = parseString(value, defaultOAuth2OidcDiscoveryEndpoint)
case "OAUTH2_OIDC_PROVIDER_NAME":
p.opts.oidcProviderName = parseString(value, defaultOauth2OidcProviderName)
case "OAUTH2_PROVIDER":
p.opts.oauth2Provider = parseString(value, defaultOAuth2Provider)
case "DISABLE_LOCAL_AUTH":
p.opts.disableLocalAuth = parseBool(value, defaultDisableLocalAuth)
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)
case "HTTP_CLIENT_PROXY":
p.opts.httpClientProxyURL, err = url.Parse(parseString(value, defaultHTTPClientProxy))
if err != nil {
return fmt.Errorf("config: invalid HTTP_CLIENT_PROXY value: %w", err)
}
case "HTTP_CLIENT_PROXIES":
p.opts.httpClientProxies = parseStringList(value, []string{})
case "HTTP_CLIENT_USER_AGENT":
p.opts.httpClientUserAgent = parseString(value, defaultHTTPClientUserAgent)
case "HTTP_SERVER_TIMEOUT":
p.opts.httpServerTimeout = parseInt(value, defaultHTTPServerTimeout)
case "AUTH_PROXY_HEADER":
p.opts.authProxyHeader = parseString(value, defaultAuthProxyHeader)
case "AUTH_PROXY_USER_CREATION":
p.opts.authProxyUserCreation = parseBool(value, defaultAuthProxyUserCreation)
case "MAINTENANCE_MODE":
p.opts.maintenanceMode = parseBool(value, defaultMaintenanceMode)
case "MAINTENANCE_MESSAGE":
p.opts.maintenanceMessage = parseString(value, defaultMaintenanceMessage)
case "METRICS_COLLECTOR":
p.opts.metricsCollector = parseBool(value, defaultMetricsCollector)
case "METRICS_REFRESH_INTERVAL":
p.opts.metricsRefreshInterval = parseInt(value, defaultMetricsRefreshInterval)
case "METRICS_ALLOWED_NETWORKS":
p.opts.metricsAllowedNetworks = parseStringList(value, []string{defaultMetricsAllowedNetworks})
case "METRICS_USERNAME":
p.opts.metricsUsername = parseString(value, defaultMetricsUsername)
case "METRICS_USERNAME_FILE":
p.opts.metricsUsername = readSecretFile(value, defaultMetricsUsername)
case "METRICS_PASSWORD":
p.opts.metricsPassword = parseString(value, defaultMetricsPassword)
case "METRICS_PASSWORD_FILE":
p.opts.metricsPassword = readSecretFile(value, defaultMetricsPassword)
case "FETCH_BILIBILI_WATCH_TIME":
p.opts.fetchBilibiliWatchTime = parseBool(value, defaultFetchBilibiliWatchTime)
case "FETCH_NEBULA_WATCH_TIME":
p.opts.fetchNebulaWatchTime = parseBool(value, defaultFetchNebulaWatchTime)
case "FETCH_ODYSEE_WATCH_TIME":
p.opts.fetchOdyseeWatchTime = parseBool(value, defaultFetchOdyseeWatchTime)
case "FETCH_YOUTUBE_WATCH_TIME":
p.opts.fetchYouTubeWatchTime = parseBool(value, defaultFetchYouTubeWatchTime)
case "YOUTUBE_API_KEY":
p.opts.youTubeApiKey = parseString(value, defaultYouTubeApiKey)
case "YOUTUBE_EMBED_URL_OVERRIDE":
p.opts.youTubeEmbedUrlOverride = parseString(value, defaultYouTubeEmbedUrlOverride)
case "WATCHDOG":
p.opts.watchdog = parseBool(value, defaultWatchdog)
case "INVIDIOUS_INSTANCE":
p.opts.invidiousInstance = parseString(value, defaultInvidiousInstance)
case "WEBAUTHN":
p.opts.webAuthn = parseBool(value, defaultWebAuthn)
}
}

@jvoisin
Copy link
Collaborator Author

jvoisin commented Jul 9, 2025

What kind of unit-tests are you thinking of?

@fguillot
Copy link
Member

What kind of unit-tests are you thinking of?

Something similar to what already exists. Few test cases to verify that the parsed config values are what is expected.

func TestDisableHTTPServiceWhenUnset(t *testing.T) {
os.Clearenv()
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := true
result := opts.HasHTTPService()
if result != expected {
t.Fatalf(`Unexpected DISABLE_HTTP_SERVICE value, got %v instead of %v`, result, expected)
}
}
func TestDisableHTTPService(t *testing.T) {
os.Clearenv()
os.Setenv("DISABLE_HTTP_SERVICE", "1")
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := false
result := opts.HasHTTPService()
if result != expected {
t.Fatalf(`Unexpected DISABLE_HTTP_SERVICE value, got %v instead of %v`, result, expected)
}
}

@jvoisin jvoisin force-pushed the disable_api branch 2 times, most recently from a7f85c3 to 928af80 Compare July 10, 2025 15:26
Copy link
Member

@fguillot fguillot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Fever and Google Reader sections on the integration page should probably be hidden. Same for the Miniflux API Keys page. Any thoughts?

The web ui crashes when using DISABLE_FEVER_API=1. Needs more testing.

level=INFO msg="http: panic serving 127.0.0.1:60350: template: main:650:81: executing \"content\" at <route \"feverEndpoint\">: error calling route: route not found: feverEndpoint\ngoroutine 66 [running]:\nnet/http.(*conn).serve.func1()\n\t/usr/local/go/src/net/http/server.go:1947 +0xbe\npanic({0xd99d20?, 0xc00085e640?})\n\t/usr/local/go/src/runtime/panic.go:792 +0x132\nminiflux.app/v2/internal/template.(*Engine).Render(0xd387a0?, {0xc000db2fd0, 0x11}, 0xc000d97620)\n\t/home/fred/repos/miniflux/v2/internal/template/engine.go:135 +0x39f\nminiflux.app/v2/internal/ui/view.(*View).Render(...)\n\t/home/fred/repos/miniflux/v2/internal/ui/view/view.go:31\nminiflux.app/v2/internal/ui.(*handler).showIntegrationPage(0xc000c32860, {0x109f238, 0xc000390380}, 0xc0001def00)\n\t/home/fred/repos/miniflux/v2/internal/ui/integration_show.go:149 +0xdda\nnet/http.HandlerFunc.ServeHTTP(0xc0001dedc0?, {0x109f238?, 0xc000390380?}, 0x1092898?)\n\t/usr/local/go/src/net/http/server.go:2294 +0x29\nminiflux.app/v2/internal/ui.(*middleware).handleAppSession-fm.(*middleware).handleAppSession.func1({0x109f238, 0xc000390380}, 0xc0001dedc0)\n\t/home/fred/repos/miniflux/v2/internal/ui/middleware.go:131 +0x955\nnet/http.HandlerFunc.ServeHTTP(0xc000cf1cc0?, {0x109f238?, 0xc000390380?}, 0x10928d8?)\n\t/usr/local/go/src/net/http/server.go:2294 +0x29\nminiflux.app/v2/internal/ui.(*middleware).handleUserSession-fm.(*middleware).handleUserSession.func1({0x109f238, 0xc000390380}, 0xc000cf1cc0)\n\t/home/fred/repos/miniflux/v2/internal/ui/middleware.go:59 +0x368\nnet/http.HandlerFunc.ServeHTTP(0xc000e421e0?, {0x109f238?, 0xc000390380?}, 0x1094f58?)\n\t/usr/local/go/src/net/http/server.go:2294 +0x29\nminiflux.app/v2/internal/http/server.middleware.func1({0x109f238, 0xc000390380}, 0xc000cf1b80)\n\t/home/fred/repos/miniflux/v2/internal/http/server/middleware.go:43 +0x351\nnet/http.HandlerFunc.ServeHTTP(0xc000cf1a40?, {0x109f238?, 0xc000390380?}, 0x17612a0?)\n\t/usr/local/go/src/net/http/server.go:2294 +0x29\ngithub.com/gorilla/mux.(*Router).ServeHTTP(0xc0000d8000, {0x109f238, 0xc000390380}, 0xc000cf1900)\n\t/home/fred/go/pkg/mod/github.com/gorilla/[email protected]/mux.go:212 +0x1e2\nnet/http.serverHandler.ServeHTTP({0xc000e421b0?}, {0x109f238?, 0xc000390380?}, 0x6?)\n\t/usr/local/go/src/net/http/server.go:3301 +0x8e\nnet/http.(*conn).serve(0xc0001d7b90, {0x10a0e58, 0xc000ccc270})\n\t/usr/local/go/src/net/http/server.go:2102 +0x625\ncreated by net/http.(*Server).Serve in goroutine 35\n\t/usr/local/go/src/net/http/server.go:3454 +0x485

Some users aren't using those, and it thus makes sense to provide a way to
disable them, as they expose quite a lot of sensitive-ish features.
@jvoisin
Copy link
Collaborator Author

jvoisin commented Jul 14, 2025

I'll try to implement the UI-side of this this week :)

@jvoisin
Copy link
Collaborator Author

jvoisin commented Jul 15, 2025

Superseded by #3543

@jvoisin jvoisin closed this Jul 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants