From 9d7f54be823b046bcd3b0814634c8521b6994c56 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Wed, 26 Nov 2025 03:58:00 +0100 Subject: [PATCH] Feature/improve server config non privacy safe setting handling (#1794) * Move the "group" arg at the second position after "protoNumber" To make it consistent for all settings * Improve server config non privacy safe setting handling --------- Co-authored-by: Mitchell Syer --- .../xyz/nulldev/ts/config/ConfigManager.kt | 17 +++ .../suwayomi/tachidesk/server/ServerConfig.kt | 126 +++++++++++++++--- .../server/settings/SettingDelegate.kt | 58 ++++++-- .../server/settings/SettingsRegistry.kt | 1 + .../suwayomi/tachidesk/server/ServerSetup.kt | 20 +-- 5 files changed, 182 insertions(+), 40 deletions(-) diff --git a/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigManager.kt b/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigManager.kt index 6be0b4268..b9db127a7 100644 --- a/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigManager.kt +++ b/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigManager.kt @@ -178,6 +178,23 @@ open class ConfigManager { userConfigFile.writeText(newUserConfigDoc.render()) getUserConfig().entrySet().forEach { internalConfig = internalConfig.withValue(it.key, it.value) } } + + fun getRedactedConfig(nonPrivacySafeKeys: List): Config { + val entries = + config.entrySet().associate { entry -> + val key = entry.key + val value = + if (nonPrivacySafeKeys.any { key.split(".").getOrNull(1) == it }) { + "[REDACTED]" + } else { + entry.value.unwrapped() + } + + key to value + } + + return ConfigFactory.parseMap(entries) + } } object GlobalConfigManager : ConfigManager() diff --git a/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt b/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt index 7ce3351db..6c16cd70b 100644 --- a/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt +++ b/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt @@ -85,6 +85,7 @@ class ServerConfig( val ip: MutableStateFlow by StringSetting( protoNumber = 1, group = SettingGroup.NETWORK, + privacySafe = true, defaultValue = "0.0.0.0", pattern = "^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$".toRegex(), excludeFromBackup = true, @@ -93,6 +94,7 @@ class ServerConfig( val port: MutableStateFlow by IntSetting( protoNumber = 2, group = SettingGroup.NETWORK, + privacySafe = true, defaultValue = 4567, min = 1, max = 65535, @@ -102,12 +104,14 @@ class ServerConfig( val socksProxyEnabled: MutableStateFlow by BooleanSetting( protoNumber = 3, group = SettingGroup.PROXY, + privacySafe = true, defaultValue = false, ) val socksProxyVersion: MutableStateFlow by IntSetting( protoNumber = 4, group = SettingGroup.PROXY, + privacySafe = true, defaultValue = 5, min = 4, max = 5, @@ -116,18 +120,21 @@ class ServerConfig( val socksProxyHost: MutableStateFlow by StringSetting( protoNumber = 5, group = SettingGroup.PROXY, + privacySafe = true, defaultValue = "", ) val socksProxyPort: MutableStateFlow by StringSetting( protoNumber = 6, group = SettingGroup.PROXY, + privacySafe = true, defaultValue = "", ) val socksProxyUsername: MutableStateFlow by StringSetting( protoNumber = 7, group = SettingGroup.PROXY, + privacySafe = false, defaultValue = "", excludeFromBackup = true, ) @@ -135,6 +142,7 @@ class ServerConfig( val socksProxyPassword: MutableStateFlow by StringSetting( protoNumber = 8, group = SettingGroup.PROXY, + privacySafe = false, defaultValue = "", excludeFromBackup = true, ) @@ -142,6 +150,7 @@ class ServerConfig( val webUIFlavor: MutableStateFlow by EnumSetting( protoNumber = 9, group = SettingGroup.WEB_UI, + privacySafe = true, defaultValue = WebUIFlavor.WEBUI, enumClass = WebUIFlavor::class, typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("suwayomi.tachidesk.graphql.types.WebUIFlavor")), @@ -150,6 +159,7 @@ class ServerConfig( val initialOpenInBrowserEnabled: MutableStateFlow by BooleanSetting( protoNumber = 10, group = SettingGroup.WEB_UI, + privacySafe = true, defaultValue = true, description = "Open client on startup", ) @@ -157,6 +167,7 @@ class ServerConfig( val webUIInterface: MutableStateFlow by EnumSetting( protoNumber = 11, group = SettingGroup.WEB_UI, + privacySafe = true, defaultValue = WebUIInterface.BROWSER, enumClass = WebUIInterface::class, typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("suwayomi.tachidesk.graphql.types.WebUIInterface")), @@ -165,6 +176,7 @@ class ServerConfig( val electronPath: MutableStateFlow by PathSetting( protoNumber = 12, group = SettingGroup.WEB_UI, + privacySafe = true, defaultValue = "", mustExist = true, excludeFromBackup = true, @@ -173,6 +185,7 @@ class ServerConfig( val webUIChannel: MutableStateFlow by EnumSetting( protoNumber = 13, group = SettingGroup.WEB_UI, + privacySafe = true, defaultValue = WebUIChannel.STABLE, enumClass = WebUIChannel::class, typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("suwayomi.tachidesk.graphql.types.WebUIChannel")), @@ -181,6 +194,7 @@ class ServerConfig( val webUIUpdateCheckInterval: MutableStateFlow by DisableableDoubleSetting( protoNumber = 14, group = SettingGroup.WEB_UI, + privacySafe = true, defaultValue = 23.hours.inWholeHours.toDouble(), min = 0.0, max = 23.0, @@ -189,13 +203,15 @@ class ServerConfig( val downloadAsCbz: MutableStateFlow by BooleanSetting( protoNumber = 15, - defaultValue = false, group = SettingGroup.DOWNLOADER, + privacySafe = true, + defaultValue = false, ) val downloadsPath: MutableStateFlow by PathSetting( protoNumber = 16, group = SettingGroup.DOWNLOADER, + privacySafe = true, defaultValue = "", mustExist = true, excludeFromBackup = true, @@ -203,13 +219,15 @@ class ServerConfig( val autoDownloadNewChapters: MutableStateFlow by BooleanSetting( protoNumber = 17, - defaultValue = false, group = SettingGroup.DOWNLOADER, + privacySafe = true, + defaultValue = false, ) val excludeEntryWithUnreadChapters: MutableStateFlow by BooleanSetting( protoNumber = 18, group = SettingGroup.DOWNLOADER, + privacySafe = true, defaultValue = true, description = "Exclude entries with unread chapters from auto-download", ) @@ -217,8 +235,9 @@ class ServerConfig( @Deprecated("Will get removed", replaceWith = ReplaceWith("autoDownloadNewChaptersLimit")) val autoDownloadAheadLimit: MutableStateFlow by MigratedConfigValue( protoNumber = 19, - defaultValue = 0, group = SettingGroup.DOWNLOADER, + privacySafe = true, + defaultValue = 0, deprecated = SettingsRegistry.SettingDeprecated( replaceWith = "autoDownloadNewChaptersLimit", @@ -232,6 +251,7 @@ class ServerConfig( val autoDownloadNewChaptersLimit: MutableStateFlow by DisableableIntSetting( protoNumber = 20, group = SettingGroup.DOWNLOADER, + privacySafe = true, defaultValue = 0, min = 0, description = "Maximum number of new chapters to auto-download", @@ -240,6 +260,7 @@ class ServerConfig( val autoDownloadIgnoreReUploads: MutableStateFlow by BooleanSetting( protoNumber = 21, group = SettingGroup.DOWNLOADER, + privacySafe = true, defaultValue = false, description = "Ignore re-uploaded chapters from auto-download", ) @@ -247,6 +268,7 @@ class ServerConfig( val extensionRepos: MutableStateFlow> by ListSetting( protoNumber = 22, group = SettingGroup.EXTENSION, + privacySafe = false, defaultValue = emptyList(), itemValidator = { url -> if (url.matches(repoMatchRegex)) { @@ -272,6 +294,7 @@ class ServerConfig( val maxSourcesInParallel: MutableStateFlow by IntSetting( protoNumber = 23, group = SettingGroup.EXTENSION, + privacySafe = true, defaultValue = 6, min = 1, max = 20, @@ -282,25 +305,29 @@ class ServerConfig( val excludeUnreadChapters: MutableStateFlow by BooleanSetting( protoNumber = 24, - defaultValue = true, group = SettingGroup.LIBRARY_UPDATES, + privacySafe = true, + defaultValue = true, ) val excludeNotStarted: MutableStateFlow by BooleanSetting( protoNumber = 25, - defaultValue = true, group = SettingGroup.LIBRARY_UPDATES, + privacySafe = true, + defaultValue = true, ) val excludeCompleted: MutableStateFlow by BooleanSetting( protoNumber = 26, - defaultValue = true, group = SettingGroup.LIBRARY_UPDATES, + privacySafe = true, + defaultValue = true, ) val globalUpdateInterval: MutableStateFlow by DisableableDoubleSetting( protoNumber = 27, group = SettingGroup.LIBRARY_UPDATES, + privacySafe = true, defaultValue = 12.hours.inWholeHours.toDouble(), min = 6.0, description = "Time in hours", @@ -309,6 +336,7 @@ class ServerConfig( val updateMangas: MutableStateFlow by BooleanSetting( protoNumber = 28, group = SettingGroup.LIBRARY_UPDATES, + privacySafe = true, defaultValue = false, description = "Update manga metadata and thumbnail along with the chapter list update during the library update.", ) @@ -316,8 +344,9 @@ class ServerConfig( @Deprecated("Will get removed", replaceWith = ReplaceWith("authMode")) val basicAuthEnabled: MutableStateFlow by MigratedConfigValue( protoNumber = 29, - defaultValue = false, group = SettingGroup.AUTH, + privacySafe = true, + defaultValue = false, deprecated = SettingsRegistry.SettingDeprecated( replaceWith = "authMode", @@ -343,6 +372,7 @@ class ServerConfig( val authUsername: MutableStateFlow by StringSetting( protoNumber = 30, group = SettingGroup.AUTH, + privacySafe = false, defaultValue = "", excludeFromBackup = true, ) @@ -350,21 +380,24 @@ class ServerConfig( val authPassword: MutableStateFlow by StringSetting( protoNumber = 31, group = SettingGroup.AUTH, + privacySafe = false, defaultValue = "", excludeFromBackup = true, ) val debugLogsEnabled: MutableStateFlow by BooleanSetting( protoNumber = 32, - defaultValue = false, group = SettingGroup.MISC, + privacySafe = true, + defaultValue = false, ) @Deprecated("Removed - does not do anything") val gqlDebugLogsEnabled: MutableStateFlow by MigratedConfigValue( protoNumber = 33, - defaultValue = false, group = SettingGroup.MISC, + privacySafe = true, + defaultValue = false, deprecated = SettingsRegistry.SettingDeprecated( message = "Removed - does not do anything", @@ -373,13 +406,15 @@ class ServerConfig( val systemTrayEnabled: MutableStateFlow by BooleanSetting( protoNumber = 34, - defaultValue = true, group = SettingGroup.MISC, + privacySafe = true, + defaultValue = true, ) val maxLogFiles: MutableStateFlow by IntSetting( protoNumber = 35, group = SettingGroup.MISC, + privacySafe = true, defaultValue = 31, min = 0, description = "The max number of days to keep files before they get deleted", @@ -389,6 +424,7 @@ class ServerConfig( val maxLogFileSize: MutableStateFlow by StringSetting( protoNumber = 36, group = SettingGroup.MISC, + privacySafe = true, defaultValue = "10mb", pattern = logbackSizePattern, description = "Maximum log file size - values: 1 (bytes), 1KB (kilobytes), 1MB (megabytes), 1GB (gigabytes)", @@ -397,6 +433,7 @@ class ServerConfig( val maxLogFolderSize: MutableStateFlow by StringSetting( protoNumber = 37, group = SettingGroup.MISC, + privacySafe = true, defaultValue = "100mb", pattern = logbackSizePattern, description = "Maximum log folder size - values: 1 (bytes), 1KB (kilobytes), 1MB (megabytes), 1GB (gigabytes)", @@ -405,6 +442,7 @@ class ServerConfig( val backupPath: MutableStateFlow by PathSetting( protoNumber = 38, group = SettingGroup.BACKUP, + privacySafe = true, defaultValue = "", mustExist = true, excludeFromBackup = true, @@ -413,6 +451,7 @@ class ServerConfig( val backupTime: MutableStateFlow by StringSetting( protoNumber = 39, group = SettingGroup.BACKUP, + privacySafe = true, defaultValue = "00:00", pattern = "^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$".toRegex(), description = "Daily backup time (HH:MM) ; range: [00:00, 23:59]", @@ -421,6 +460,7 @@ class ServerConfig( val backupInterval: MutableStateFlow by DisableableIntSetting( protoNumber = 40, group = SettingGroup.BACKUP, + privacySafe = true, defaultValue = 1, min = 0, description = "Time in days", @@ -429,6 +469,7 @@ class ServerConfig( val backupTTL: MutableStateFlow by DisableableIntSetting( protoNumber = 41, group = SettingGroup.BACKUP, + privacySafe = true, defaultValue = 14.days.inWholeDays.toInt(), min = 0, description = "Backup retention in days", @@ -437,6 +478,7 @@ class ServerConfig( val localSourcePath: MutableStateFlow by PathSetting( protoNumber = 42, group = SettingGroup.LOCAL_SOURCE, + privacySafe = true, defaultValue = "", mustExist = true, excludeFromBackup = true, @@ -444,20 +486,23 @@ class ServerConfig( val flareSolverrEnabled: MutableStateFlow by BooleanSetting( protoNumber = 43, - defaultValue = false, group = SettingGroup.CLOUDFLARE, + privacySafe = true, + defaultValue = false, excludeFromBackup = true, ) val flareSolverrUrl: MutableStateFlow by StringSetting( protoNumber = 44, group = SettingGroup.CLOUDFLARE, + privacySafe = true, defaultValue = "http://localhost:8191", ) val flareSolverrTimeout: MutableStateFlow by IntSetting( protoNumber = 45, group = SettingGroup.CLOUDFLARE, + privacySafe = true, defaultValue = 60.seconds.inWholeSeconds.toInt(), min = 0, description = "Time in seconds", @@ -466,12 +511,14 @@ class ServerConfig( val flareSolverrSessionName: MutableStateFlow by StringSetting( protoNumber = 46, group = SettingGroup.CLOUDFLARE, + privacySafe = true, defaultValue = "suwayomi", ) val flareSolverrSessionTtl: MutableStateFlow by IntSetting( protoNumber = 47, group = SettingGroup.CLOUDFLARE, + privacySafe = true, defaultValue = 15.minutes.inWholeMinutes.toInt(), min = 0, description = "Time in minutes", @@ -479,13 +526,15 @@ class ServerConfig( val flareSolverrAsResponseFallback: MutableStateFlow by BooleanSetting( protoNumber = 48, - defaultValue = false, group = SettingGroup.CLOUDFLARE, + privacySafe = true, + defaultValue = false, ) val opdsUseBinaryFileSizes: MutableStateFlow by BooleanSetting( protoNumber = 49, group = SettingGroup.OPDS, + privacySafe = true, defaultValue = false, description = "Display file size in binary (KiB, MiB, GiB) instead of decimal (KB, MB, GB)", ) @@ -493,6 +542,7 @@ class ServerConfig( val opdsItemsPerPage: MutableStateFlow by IntSetting( protoNumber = 50, group = SettingGroup.OPDS, + privacySafe = true, defaultValue = 100, min = 10, max = 5000, @@ -500,31 +550,36 @@ class ServerConfig( val opdsEnablePageReadProgress: MutableStateFlow by BooleanSetting( protoNumber = 51, - defaultValue = true, group = SettingGroup.OPDS, + privacySafe = true, + defaultValue = true, ) val opdsMarkAsReadOnDownload: MutableStateFlow by BooleanSetting( protoNumber = 52, - defaultValue = false, group = SettingGroup.OPDS, + privacySafe = true, + defaultValue = false, ) val opdsShowOnlyUnreadChapters: MutableStateFlow by BooleanSetting( protoNumber = 53, - defaultValue = false, group = SettingGroup.OPDS, + privacySafe = true, + defaultValue = false, ) val opdsShowOnlyDownloadedChapters: MutableStateFlow by BooleanSetting( protoNumber = 54, - defaultValue = false, group = SettingGroup.OPDS, + privacySafe = true, + defaultValue = false, ) val opdsChapterSortOrder: MutableStateFlow by EnumSetting( protoNumber = 55, group = SettingGroup.OPDS, + privacySafe = true, defaultValue = SortOrder.DESC, enumClass = SortOrder::class, typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("org.jetbrains.exposed.sql.SortOrder")), @@ -533,6 +588,7 @@ class ServerConfig( val authMode: MutableStateFlow by EnumSetting( protoNumber = 56, group = SettingGroup.AUTH, + privacySafe = true, defaultValue = AuthMode.NONE, enumClass = AuthMode::class, typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("suwayomi.tachidesk.graphql.types.AuthMode")), @@ -541,8 +597,9 @@ class ServerConfig( fun createDownloadConversionsMap(protoNumber: Int, key: String) = MapSetting( protoNumber = protoNumber, - defaultValue = emptyMap(), group = SettingGroup.DOWNLOADER, + privacySafe = false, + defaultValue = emptyMap(), typeInfo = SettingsRegistry.PartialTypeInfo( specificType = "List", @@ -627,12 +684,14 @@ class ServerConfig( val jwtAudience: MutableStateFlow by StringSetting( protoNumber = 58, group = SettingGroup.AUTH, + privacySafe = true, defaultValue = "suwayomi-server-api", ) val koreaderSyncServerUrl: MutableStateFlow by StringSetting( protoNumber = 59, group = SettingGroup.KOREADER_SYNC, + privacySafe = true, defaultValue = "https://sync.koreader.rocks/", description = "KOReader Sync Server URL. Public alternative: https://kosync.ak-team.com:3042/", ) @@ -641,6 +700,7 @@ class ServerConfig( val koreaderSyncUsername: MutableStateFlow by MigratedConfigValue( protoNumber = 60, group = SettingGroup.KOREADER_SYNC, + privacySafe = false, defaultValue = "", deprecated = SettingsRegistry.SettingDeprecated( replaceWith = "MOVE TO PREFERENCES", @@ -658,6 +718,7 @@ class ServerConfig( val koreaderSyncUserkey: MutableStateFlow by MigratedConfigValue( protoNumber = 61, group = SettingGroup.KOREADER_SYNC, + privacySafe = false, defaultValue = "", deprecated = SettingsRegistry.SettingDeprecated( replaceWith = "MOVE TO PREFERENCES", @@ -675,6 +736,7 @@ class ServerConfig( val koreaderSyncDeviceId: MutableStateFlow by MigratedConfigValue( protoNumber = 62, group = SettingGroup.KOREADER_SYNC, + privacySafe = true, defaultValue = "", deprecated = SettingsRegistry.SettingDeprecated( replaceWith = "MOVE TO PREFERENCES", @@ -691,6 +753,7 @@ class ServerConfig( val koreaderSyncChecksumMethod: MutableStateFlow by EnumSetting( protoNumber = 63, group = SettingGroup.KOREADER_SYNC, + privacySafe = true, defaultValue = KoreaderSyncChecksumMethod.BINARY, enumClass = KoreaderSyncChecksumMethod::class, typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("suwayomi.tachidesk.graphql.types.KoreaderSyncChecksumMethod")), @@ -700,8 +763,9 @@ class ServerConfig( @Deprecated("Use koreaderSyncStrategyForward and koreaderSyncStrategyBackward instead") val koreaderSyncStrategy: MutableStateFlow by MigratedConfigValue( protoNumber = 64, - defaultValue = KoreaderSyncLegacyStrategy.DISABLED, group = SettingGroup.KOREADER_SYNC, + privacySafe = true, + defaultValue = KoreaderSyncLegacyStrategy.DISABLED, typeInfo = SettingsRegistry.PartialTypeInfo( imports = listOf("suwayomi.tachidesk.graphql.types.KoreaderSyncLegacyStrategy"), @@ -772,6 +836,7 @@ class ServerConfig( val koreaderSyncPercentageTolerance: MutableStateFlow by DoubleSetting( protoNumber = 65, group = SettingGroup.KOREADER_SYNC, + privacySafe = true, defaultValue = 0.000000000000001, min = 0.000000000000001, max = 1.0, @@ -781,6 +846,7 @@ class ServerConfig( val jwtTokenExpiry: MutableStateFlow by DurationSetting( protoNumber = 66, group = SettingGroup.AUTH, + privacySafe = true, defaultValue = 5.minutes, min = 0.seconds, ) @@ -788,6 +854,7 @@ class ServerConfig( val jwtRefreshExpiry: MutableStateFlow by DurationSetting( protoNumber = 67, group = SettingGroup.AUTH, + privacySafe = true, defaultValue = 60.days, min = 0.seconds, ) @@ -795,6 +862,7 @@ class ServerConfig( val webUIEnabled: MutableStateFlow by BooleanSetting( protoNumber = 68, group = SettingGroup.WEB_UI, + privacySafe = true, defaultValue = true, requiresRestart = true, ) @@ -802,6 +870,7 @@ class ServerConfig( val databaseType: MutableStateFlow by EnumSetting( protoNumber = 69, group = SettingGroup.DATABASE, + privacySafe = true, defaultValue = DatabaseType.H2, enumClass = DatabaseType::class, typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("suwayomi.tachidesk.graphql.types.DatabaseType")), @@ -811,6 +880,7 @@ class ServerConfig( val databaseUrl: MutableStateFlow by StringSetting( protoNumber = 70, group = SettingGroup.DATABASE, + privacySafe = true, defaultValue = "postgresql://localhost:5432/suwayomi", excludeFromBackup = true, ) @@ -818,6 +888,7 @@ class ServerConfig( val databaseUsername: MutableStateFlow by StringSetting( protoNumber = 71, group = SettingGroup.DATABASE, + privacySafe = false, defaultValue = "", excludeFromBackup = true, ) @@ -825,6 +896,7 @@ class ServerConfig( val databasePassword: MutableStateFlow by StringSetting( protoNumber = 72, group = SettingGroup.DATABASE, + privacySafe = false, defaultValue = "", excludeFromBackup = true, ) @@ -832,6 +904,7 @@ class ServerConfig( val koreaderSyncStrategyForward: MutableStateFlow by EnumSetting( protoNumber = 73, group = SettingGroup.KOREADER_SYNC, + privacySafe = true, defaultValue = KoreaderSyncConflictStrategy.PROMPT, enumClass = KoreaderSyncConflictStrategy::class, typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("suwayomi.tachidesk.graphql.types.KoreaderSyncConflictStrategy")), @@ -841,6 +914,7 @@ class ServerConfig( val koreaderSyncStrategyBackward: MutableStateFlow by EnumSetting( protoNumber = 74, group = SettingGroup.KOREADER_SYNC, + privacySafe = true, defaultValue = KoreaderSyncConflictStrategy.DISABLED, enumClass = KoreaderSyncConflictStrategy::class, typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("suwayomi.tachidesk.graphql.types.KoreaderSyncConflictStrategy")), @@ -850,6 +924,7 @@ class ServerConfig( val webUISubpath: MutableStateFlow by StringSetting( protoNumber = 75, group = SettingGroup.WEB_UI, + privacySafe = true, defaultValue = "", pattern = "^(/[a-zA-Z0-9._-]+)*$".toRegex(), description = "Serve WebUI under a subpath (e.g., /manga). Leave empty for root path. Must start with / if specified.", @@ -860,48 +935,56 @@ class ServerConfig( val autoBackupIncludeManga: MutableStateFlow by BooleanSetting( protoNumber = 76, group = SettingGroup.BACKUP, + privacySafe = true, defaultValue = BackupFlags.DEFAULT.includeManga, ) val autoBackupIncludeCategories: MutableStateFlow by BooleanSetting( protoNumber = 77, group = SettingGroup.BACKUP, + privacySafe = true, defaultValue = BackupFlags.DEFAULT.includeCategories, ) val autoBackupIncludeChapters: MutableStateFlow by BooleanSetting( protoNumber = 78, group = SettingGroup.BACKUP, + privacySafe = true, defaultValue = BackupFlags.DEFAULT.includeChapters, ) val autoBackupIncludeTracking: MutableStateFlow by BooleanSetting( protoNumber = 79, group = SettingGroup.BACKUP, + privacySafe = true, defaultValue = BackupFlags.DEFAULT.includeTracking, ) val autoBackupIncludeHistory: MutableStateFlow by BooleanSetting( protoNumber = 80, group = SettingGroup.BACKUP, + privacySafe = true, defaultValue = BackupFlags.DEFAULT.includeHistory, ) val autoBackupIncludeClientData: MutableStateFlow by BooleanSetting( protoNumber = 81, group = SettingGroup.BACKUP, + privacySafe = true, defaultValue = BackupFlags.DEFAULT.includeClientData, ) val autoBackupIncludeServerSettings: MutableStateFlow by BooleanSetting( protoNumber = 82, group = SettingGroup.BACKUP, + privacySafe = true, defaultValue = BackupFlags.DEFAULT.includeServerSettings, ) val opdsCbzMimetype: MutableStateFlow by EnumSetting( protoNumber = 83, group = SettingGroup.OPDS, + privacySafe = true, defaultValue = CbzMediaType.MODERN, enumClass = CbzMediaType::class, typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("suwayomi.tachidesk.graphql.types.CbzMediaType")), @@ -917,6 +1000,7 @@ class ServerConfig( val useHikariConnectionPool: MutableStateFlow by BooleanSetting( protoNumber = 85, group = SettingGroup.DATABASE, + privacySafe = true, defaultValue = true, excludeFromBackup = true, description = "Use Hikari Connection Pool to connect to the database.", @@ -933,8 +1017,9 @@ class ServerConfig( @Deprecated("Removed - prefer authUsername", replaceWith = ReplaceWith("authUsername")) val basicAuthUsername: MutableStateFlow by MigratedConfigValue( protoNumber = 99991, - defaultValue = "", group = SettingGroup.AUTH, + privacySafe = false, + defaultValue = "", deprecated = SettingsRegistry.SettingDeprecated( replaceWith = "authUsername", @@ -948,8 +1033,9 @@ class ServerConfig( @Deprecated("Removed - prefer authPassword", replaceWith = ReplaceWith("authPassword")) val basicAuthPassword: MutableStateFlow by MigratedConfigValue( protoNumber = 99992, - defaultValue = "", group = SettingGroup.AUTH, + privacySafe = false, + defaultValue = "", deprecated = SettingsRegistry.SettingDeprecated( replaceWith = "authPassword", diff --git a/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/settings/SettingDelegate.kt b/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/settings/SettingDelegate.kt index 98fc9f2b9..c273fd15c 100644 --- a/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/settings/SettingDelegate.kt +++ b/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/settings/SettingDelegate.kt @@ -16,6 +16,14 @@ import kotlin.reflect.KClass import kotlin.reflect.KProperty import kotlin.time.Duration +private fun maybeRedact(value: Any, privacySafe: Boolean): String { + return if (privacySafe) { + value.toString() + } else { + "[REDACTED]" + } +} + /** * Base delegate for settings to read values from the config file with automatic setting registration and validation */ @@ -30,6 +38,7 @@ open class SettingDelegate( protected val deprecated: SettingsRegistry.SettingDeprecated? = null, protected val description: String? = null, protected val excludeFromBackup: Boolean? = null, + val privacySafe: Boolean, ) { var flow: MutableStateFlow? = null lateinit var propertyName: String @@ -83,7 +92,8 @@ open class SettingDelegate( defaultValueComment } }, - excludeFromBackup = excludeFromBackup + excludeFromBackup = excludeFromBackup, + privacySafe = privacySafe, ), ) @@ -145,6 +155,7 @@ class MigratedConfigValue( private val deprecated: SettingsRegistry.SettingDeprecated, private val readMigrated: (() -> T) = { defaultValue }, private val setMigrated: ((T) -> Unit) = {}, + private val privacySafe: Boolean ) { var flow: MutableStateFlow? = null lateinit var propertyName: String @@ -174,6 +185,7 @@ class MigratedConfigValue( deprecated = deprecated, requiresRestart = requiresRestart ?: false, excludeFromBackup = null, + privacySafe = privacySafe, ), ) @@ -215,15 +227,16 @@ class StringSetting( requiresRestart: Boolean? = null, description: String? = null, excludeFromBackup: Boolean? = null, + privacySafe: Boolean, ) : SettingDelegate( protoNumber = protoNumber, defaultValue = defaultValue, validator = { value -> when { pattern != null && !value.matches(pattern) -> - "Value must match pattern: ${pattern.pattern}" + "Value (${maybeRedact(value, privacySafe)}) must match pattern: ${pattern.pattern}" maxLength != null && value.length > maxLength -> - "Value must not exceed $maxLength characters" + "Value (${maybeRedact(value, privacySafe)}) must not exceed $maxLength characters" else -> null } }, @@ -239,6 +252,7 @@ class StringSetting( requiresRestart = requiresRestart, description = description, excludeFromBackup = excludeFromBackup, + privacySafe = privacySafe, ) abstract class RangeSetting>( @@ -254,14 +268,15 @@ abstract class RangeSetting>( requiresRestart: Boolean? = null, description: String? = null, excludeFromBackup: Boolean? = null, + privacySafe: Boolean, ) : SettingDelegate( protoNumber = protoNumber, defaultValue = defaultValue, validator = validator ?: { value -> when { - min != null && value < min -> "Value must be at least $min" - max != null && value > max -> "Value must not exceed $max" + min != null && value < min -> "Value (${maybeRedact(value, privacySafe)}) must be at least $min" + max != null && value > max -> "Value (${maybeRedact(value, privacySafe)}) must not exceed $max" else -> null } }, @@ -287,6 +302,7 @@ abstract class RangeSetting>( } }, excludeFromBackup = excludeFromBackup, + privacySafe = privacySafe, ) class IntSetting( @@ -301,6 +317,7 @@ class IntSetting( requiresRestart: Boolean? = null, description: String? = null, excludeFromBackup: Boolean? = null, + privacySafe: Boolean, ) : RangeSetting( protoNumber = protoNumber, defaultValue = defaultValue, @@ -313,6 +330,7 @@ class IntSetting( requiresRestart = requiresRestart, description = description, excludeFromBackup = excludeFromBackup, + privacySafe = privacySafe, ) class DisableableIntSetting( @@ -325,6 +343,7 @@ class DisableableIntSetting( requiresRestart: Boolean? = null, description: String? = null, excludeFromBackup: Boolean? = null, + privacySafe: Boolean, ) : RangeSetting( protoNumber = protoNumber, defaultValue = defaultValue, @@ -333,8 +352,8 @@ class DisableableIntSetting( validator = { value -> when { value == 0 -> null - min != null && value < min -> "Value must be 0.0 or at least $min" - max != null && value > max -> "Value must be 0.0 or not exceed $max" + min != null && value < min -> "Value (${maybeRedact(value, privacySafe)}) must be 0.0 or at least $min" + max != null && value > max -> "Value (${maybeRedact(value, privacySafe)}) must be 0.0 or not exceed $max" else -> null } }, @@ -360,6 +379,7 @@ class DisableableIntSetting( } }, excludeFromBackup = excludeFromBackup, + privacySafe = privacySafe, ) class DoubleSetting( @@ -374,6 +394,7 @@ class DoubleSetting( requiresRestart: Boolean? = null, description: String? = null, excludeFromBackup: Boolean? = null, + privacySafe: Boolean, ) : RangeSetting( protoNumber = protoNumber, defaultValue = defaultValue, @@ -386,6 +407,7 @@ class DoubleSetting( requiresRestart = requiresRestart, description = description, excludeFromBackup = excludeFromBackup, + privacySafe = privacySafe, ) class DisableableDoubleSetting( @@ -398,6 +420,7 @@ class DisableableDoubleSetting( requiresRestart: Boolean? = null, description: String? = null, excludeFromBackup: Boolean? = null, + privacySafe: Boolean, ) : RangeSetting( protoNumber = protoNumber, defaultValue = defaultValue, @@ -406,8 +429,8 @@ class DisableableDoubleSetting( validator = { value -> when { value == 0.0 -> null - min != null && value < min -> "Value must be 0.0 or be at least $min" - max != null && value > max -> "Value must be 0.0 or not exceed $max" + min != null && value < min -> "Value (${maybeRedact(value, privacySafe)}) must be 0.0 or be at least $min" + max != null && value > max -> "Value (${maybeRedact(value, privacySafe)}) must be 0.0 or not exceed $max" else -> null } }, @@ -433,6 +456,7 @@ class DisableableDoubleSetting( } }, excludeFromBackup = excludeFromBackup, + privacySafe = privacySafe, ) class BooleanSetting( @@ -443,6 +467,7 @@ class BooleanSetting( requiresRestart: Boolean? = null, description: String? = null, excludeFromBackup: Boolean? = null, + privacySafe: Boolean, ) : SettingDelegate( protoNumber = protoNumber, defaultValue = defaultValue, @@ -452,6 +477,7 @@ class BooleanSetting( requiresRestart = requiresRestart, description = description, excludeFromBackup = excludeFromBackup, + privacySafe = privacySafe, ) class PathSetting( @@ -463,12 +489,13 @@ class PathSetting( requiresRestart: Boolean? = null, description: String? = null, excludeFromBackup: Boolean? = null, + privacySafe: Boolean, ) : SettingDelegate( protoNumber = protoNumber, defaultValue = defaultValue, validator = { value -> if (mustExist && value.isNotEmpty() && !File(value).exists()) { - "Path does not exist: $value" + "Path does not exist: ${maybeRedact(value, privacySafe)}" } else { null } @@ -478,6 +505,7 @@ class PathSetting( requiresRestart = requiresRestart, description = description, excludeFromBackup = excludeFromBackup, + privacySafe = privacySafe, ) class EnumSetting>( @@ -490,12 +518,13 @@ class EnumSetting>( requiresRestart: Boolean? = null, description: String? = null, excludeFromBackup: Boolean? = null, + privacySafe: Boolean, ) : SettingDelegate( protoNumber = protoNumber, defaultValue = defaultValue, validator = { value -> if (!enumClass.java.isInstance(value)) { - "Invalid enum value for ${enumClass.simpleName}" + "Invalid enum value (${maybeRedact(value, privacySafe)}) for ${enumClass.simpleName}" } else { null } @@ -515,6 +544,7 @@ class EnumSetting>( } }, excludeFromBackup = excludeFromBackup, + privacySafe = privacySafe, ) class DurationSetting( @@ -529,6 +559,7 @@ class DurationSetting( requiresRestart: Boolean? = null, description: String? = null, excludeFromBackup: Boolean? = null, + privacySafe: Boolean, ) : RangeSetting( protoNumber = protoNumber, defaultValue = defaultValue, @@ -545,6 +576,7 @@ class DurationSetting( requiresRestart = requiresRestart, description = description, excludeFromBackup = excludeFromBackup, + privacySafe = privacySafe, ) class ListSetting( @@ -558,6 +590,7 @@ class ListSetting( requiresRestart: Boolean? = null, description: String? = null, excludeFromBackup: Boolean? = null, + privacySafe: Boolean, ) : SettingDelegate>( protoNumber = protoNumber, defaultValue = defaultValue, @@ -583,6 +616,7 @@ class ListSetting( requiresRestart = requiresRestart, description = description, excludeFromBackup = excludeFromBackup, + privacySafe = privacySafe, ) class MapSetting( @@ -595,6 +629,7 @@ class MapSetting( requiresRestart: Boolean? = null, description: String? = null, excludeFromBackup: Boolean? = null, + privacySafe: Boolean, ) : SettingDelegate>( protoNumber = protoNumber, defaultValue = defaultValue, @@ -605,4 +640,5 @@ class MapSetting( requiresRestart = requiresRestart, description = description, excludeFromBackup = excludeFromBackup, + privacySafe = privacySafe, ) diff --git a/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/settings/SettingsRegistry.kt b/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/settings/SettingsRegistry.kt index f6b0bb46a..8a439e619 100644 --- a/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/settings/SettingsRegistry.kt +++ b/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/settings/SettingsRegistry.kt @@ -74,6 +74,7 @@ object SettingsRegistry { val requiresRestart: Boolean, val description: String? = null, val excludeFromBackup: Boolean? = null, + val privacySafe: Boolean ) private val settings = mutableMapOf() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt index e96e0382e..7e1f59122 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt @@ -272,13 +272,15 @@ fun applicationSetup() { logger.debug { "Loaded config:\n" + - GlobalConfigManager.config - .root() + GlobalConfigManager + .getRedactedConfig( + SettingsRegistry + .getAll() + .filter { !it.value.privacySafe } + .keys + .toList(), + ).root() .render(ConfigRenderOptions.concise().setFormatted(true)) - .replace( - Regex("(\".*(?i:username|password).*\"\\s:\\s)\".*\""), - "$1\"[REDACTED]\"", - ) } logger.debug { "Data Root directory is set to: ${applicationDirs.dataRoot}" } @@ -409,9 +411,9 @@ fun applicationSetup() { vargs[4] as Boolean, ) }.distinctUntilChanged(), - { (databaseType, databaseUrl, databaseUsername, _, hikariCp) -> + { (databaseType, databaseUrl, _databaseUsername, _databasePassword, hikariCp) -> logger.info { - "Database changed - type=$databaseType url=$databaseUrl, username=$databaseUsername, password=[REDACTED], hikaricp=$hikariCp" + "Database changed - type=$databaseType url=$databaseUrl, username=[REDACTED], password=[REDACTED], hikaricp=$hikariCp" } databaseUp() @@ -464,7 +466,7 @@ fun applicationSetup() { }.distinctUntilChanged(), { (proxyEnabled, proxyVersion, proxyHost, proxyPort, proxyUsername, proxyPassword) -> logger.info { - "Socks Proxy changed - enabled=$proxyEnabled address=$proxyHost:$proxyPort , username=$proxyUsername, password=[REDACTED]" + "Socks Proxy changed - enabled=$proxyEnabled address=$proxyHost:$proxyPort , username=[REDACTED], password=[REDACTED]" } if (proxyEnabled) { System.setProperty("socksProxyHost", proxyHost)