Files
ersatztv/ErsatzTV.Core/FileSystemLayout.cs
T
timothyandClaude Opus 4.8 37155c866b security(#197): fail-closed API auth, sensitive-read tier, CORS/ForwardedHeaders lockdown (Bundle A)
Backend of #197 Bundle A (auth posture). Owner decisions: single API key;
Api:RequireKeyForReads defaults true (whole /api surface gated; /iptv streaming
+ guide unaffected — outside the filter's /api scope).

- #280 S1: writes are fail-closed. New IApiKeyProvider resolves the key once
  (Api:WriteKey config, else persisted /config/api.key, else a generated 256-bit
  key written 0600). The empty-key open branch is gone; there is no open mode.
- #282 S3/S5: reads under /api require the key when Api:RequireKeyForReads (default
  true) or the endpoint carries the new [RequiresApiKey]. Applied [RequiresApiKey]
  to Troubleshoot/Logs/Settings/Maintenance so the sensitive tier stays gated even
  if reads are opened. OPTIONS preflight is exempt.
- #281 S2: delete SortController (dead Blazor SortableJS residue; SPA uses PUT
  /api/collections/{id}/custom-order) and AccountController (dead OIDC logout) —
  both non-/api persistent surfaces that bypassed the key.
- #284 S6: replace CORS AllowAll with an opt-in exact-origin allowlist
  (Api:CorsAllowedOrigins; permits X-Api-Key/If-Match, exposes ETag). Default is
  no cross-origin (SPA is same-origin).
- #285 S7/S10: gc GET->POST (spec regenerated); ForwardedHeaders trust configurable
  via ForwardedHeaders:KnownProxies/KnownNetworks (warns when unrestricted);
  ScannerController gains [LocalhostOnly] (scanner always calls back over localhost).

Filter unit tests rewritten for fail-closed + read-gating + tier + OPTIONS;
ApiControllerSecurityTests assert the sensitive tier + scanner-loopback reflectively.
search/all-items paging deferred (SPA add-all coupling) — exposure closed by read-gating.

Refs #197 #280 #281 #282 #284 #285

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:03:55 +02:00

201 lines
8.3 KiB
C#

using System.Reflection;
using Serilog;
namespace ErsatzTV.Core;
public static class FileSystemLayout
{
public static readonly string AppDataFolder;
public static readonly string TranscodeFolder;
public static readonly string TranscodeTroubleshootingChannel;
public static readonly string TranscodeTroubleshootingFolder;
public static readonly string DataProtectionFolder;
public static readonly string LogsFolder;
public static readonly string DatabasePath;
public static readonly string LogFilePath;
public static readonly string LegacyImageCacheFolder;
public static readonly string ResourcesCacheFolder;
public static readonly string ChannelGuideCacheFolder;
public static readonly string PlexSecretsPath;
public static readonly string JellyfinSecretsPath;
public static readonly string EmbySecretsPath;
public static readonly string ApiKeyPath;
public static readonly string FFmpegReportsFolder;
public static readonly string SearchIndexFolder;
public static readonly string TempFilePoolFolder;
public static readonly string ArtworkCacheFolder;
public static readonly string PosterCacheFolder;
public static readonly string ThumbnailCacheFolder;
public static readonly string LogoCacheFolder;
public static readonly string FanArtCacheFolder;
public static readonly string WatermarkCacheFolder;
public static readonly string StreamsCacheFolder;
public static readonly string SubtitleCacheFolder;
public static readonly string FontsCacheFolder;
public static readonly string TemplatesFolder;
public static readonly string MusicVideoCreditsTemplatesFolder;
public static readonly string ChannelGuideTemplatesFolder;
public static readonly string GraphicsElementsTemplatesFolder;
public static readonly string GraphicsElementsTextTemplatesFolder;
public static readonly string GraphicsElementsImageTemplatesFolder;
public static readonly string GraphicsElementsScriptTemplatesFolder;
public static readonly string GraphicsElementsSubtitleTemplatesFolder;
public static readonly string GraphicsElementsMotionTemplatesFolder;
public static readonly string ScriptsFolder;
public static readonly string MultiEpisodeShuffleTemplatesFolder;
public static readonly string AudioStreamSelectorScriptsFolder;
public static readonly string ChannelStreamSelectorsFolder;
public static readonly string MpegTsScriptsFolder;
public static readonly string DefaultMpegTsScriptFolder;
public static readonly string MacOsOldAppDataFolder = Path.Combine(
Environment.GetEnvironmentVariable("HOME") ?? string.Empty,
".local",
"share",
"ersatztv");
public static readonly string MacOsOldDatabasePath = Path.Combine(MacOsOldAppDataFolder, "ersatztv.sqlite3");
static FileSystemLayout()
{
string version = Assembly.GetEntryAssembly()?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion ?? "unknown";
bool isDocker = version.Contains("docker", StringComparison.OrdinalIgnoreCase);
string defaultConfigFolder = Path.Combine(
Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData,
Environment.SpecialFolderOption.DoNotVerify),
"ersatztv");
string customConfigFolder = SystemEnvironment.ConfigFolder;
bool useCustomConfigFolder = !string.IsNullOrWhiteSpace(customConfigFolder);
if (useCustomConfigFolder && isDocker)
{
// check for config at old location
if (Directory.Exists(defaultConfigFolder))
{
Log.Logger.Warning(
"Ignoring ETV_CONFIG_FOLDER {Folder} and using default {Default}",
customConfigFolder,
defaultConfigFolder);
// ignore custom config folder
useCustomConfigFolder = false;
}
}
AppDataFolder = useCustomConfigFolder ? customConfigFolder : defaultConfigFolder;
string defaultTranscodeFolder = Path.Combine(
Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData,
Environment.SpecialFolderOption.DoNotVerify),
"etv-transcode");
string customTranscodeFolder = SystemEnvironment.TranscodeFolder;
bool useCustomTranscodeFolder = !string.IsNullOrWhiteSpace(customTranscodeFolder);
if (useCustomTranscodeFolder && isDocker)
{
// check for config at old location
if (Directory.Exists(defaultTranscodeFolder))
{
Log.Logger.Warning(
"Ignoring ETV_TRANSCODE_FOLDER {Folder} and using default {Default}",
customTranscodeFolder,
defaultTranscodeFolder);
// ignore custom config folder
useCustomTranscodeFolder = false;
}
}
TranscodeFolder = useCustomTranscodeFolder ? customTranscodeFolder : defaultTranscodeFolder;
TranscodeTroubleshootingChannel = ".troubleshooting";
TranscodeTroubleshootingFolder = Path.Combine(TranscodeFolder, TranscodeTroubleshootingChannel);
DataProtectionFolder = Path.Combine(AppDataFolder, "data-protection");
LogsFolder = Path.Combine(AppDataFolder, "logs");
DatabasePath = Path.Combine(AppDataFolder, "ersatztv.sqlite3");
LogFilePath = Path.Combine(LogsFolder, "ersatztv.log");
LegacyImageCacheFolder = Path.Combine(AppDataFolder, "cache", "images");
ResourcesCacheFolder = Path.Combine(AppDataFolder, "cache", "resources");
ChannelGuideCacheFolder = Path.Combine(AppDataFolder, "cache", "channel-guide");
PlexSecretsPath = Path.Combine(AppDataFolder, "plex-secrets.json");
JellyfinSecretsPath = Path.Combine(AppDataFolder, "jellyfin-secrets.json");
EmbySecretsPath = Path.Combine(AppDataFolder, "emby-secrets.json");
ApiKeyPath = Path.Combine(AppDataFolder, "api.key");
FFmpegReportsFolder = Path.Combine(AppDataFolder, "ffmpeg-reports");
SearchIndexFolder = Path.Combine(AppDataFolder, "search-index");
TempFilePoolFolder = Path.Combine(AppDataFolder, "temp-pool");
ArtworkCacheFolder = Path.Combine(AppDataFolder, "cache", "artwork");
ArtworkCacheFolder = Path.Combine(AppDataFolder, "cache", "artwork");
PosterCacheFolder = Path.Combine(ArtworkCacheFolder, "posters");
ThumbnailCacheFolder = Path.Combine(ArtworkCacheFolder, "thumbnails");
LogoCacheFolder = Path.Combine(ArtworkCacheFolder, "logos");
FanArtCacheFolder = Path.Combine(ArtworkCacheFolder, "fanart");
WatermarkCacheFolder = Path.Combine(ArtworkCacheFolder, "watermarks");
StreamsCacheFolder = Path.Combine(AppDataFolder, "cache", "streams");
SubtitleCacheFolder = Path.Combine(StreamsCacheFolder, "subtitles");
FontsCacheFolder = Path.Combine(StreamsCacheFolder, "fonts");
TemplatesFolder = Path.Combine(AppDataFolder, "templates");
MusicVideoCreditsTemplatesFolder = Path.Combine(TemplatesFolder, "music-video-credits");
ChannelGuideTemplatesFolder = Path.Combine(TemplatesFolder, "channel-guide");
GraphicsElementsTemplatesFolder = Path.Combine(TemplatesFolder, "graphics-elements");
GraphicsElementsTextTemplatesFolder = Path.Combine(GraphicsElementsTemplatesFolder, "text");
GraphicsElementsImageTemplatesFolder = Path.Combine(GraphicsElementsTemplatesFolder, "image");
GraphicsElementsScriptTemplatesFolder = Path.Combine(GraphicsElementsTemplatesFolder, "script");
GraphicsElementsSubtitleTemplatesFolder = Path.Combine(GraphicsElementsTemplatesFolder, "subtitle");
GraphicsElementsMotionTemplatesFolder = Path.Combine(GraphicsElementsTemplatesFolder, "motion");
ScriptsFolder = Path.Combine(AppDataFolder, "scripts");
MultiEpisodeShuffleTemplatesFolder = Path.Combine(ScriptsFolder, "multi-episode-shuffle");
AudioStreamSelectorScriptsFolder = Path.Combine(ScriptsFolder, "audio-stream-selector");
ChannelStreamSelectorsFolder = Path.Combine(ScriptsFolder, "channel-stream-selectors");
MpegTsScriptsFolder = Path.Combine(ScriptsFolder, "mpegts");
DefaultMpegTsScriptFolder = Path.Combine(MpegTsScriptsFolder, "default");
}
}