diff --git a/TinfoilVibeServer.sln.DotSettings.user b/TinfoilVibeServer.sln.DotSettings.user index a5c21f9..dc418dc 100644 --- a/TinfoilVibeServer.sln.DotSettings.user +++ b/TinfoilVibeServer.sln.DotSettings.user @@ -7,6 +7,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded diff --git a/TinfoilVibeServer/Authentication/AuthStore.cs b/TinfoilVibeServer/Authentication/AuthStore.cs index 1615931..2fd3518 100644 --- a/TinfoilVibeServer/Authentication/AuthStore.cs +++ b/TinfoilVibeServer/Authentication/AuthStore.cs @@ -20,6 +20,7 @@ namespace TinfoilVibeServer.Authentication; /// public sealed class AuthStore : IDisposable { + private readonly ILogger _logger; public readonly AuthSettings Settings; public readonly ConcurrentDictionary Credentials = new(); @@ -30,8 +31,9 @@ public sealed class AuthStore : IDisposable private readonly object _sync = new(); private readonly FileSystemWatcher _credentialsWatcher; - public AuthStore() + public AuthStore(ILogger logger) { + _logger = logger; Settings = new AuthSettings( "credentials.json", "fingerprints.json", @@ -61,6 +63,7 @@ public sealed class AuthStore : IDisposable private void LoadAll() { + _logger.LogInformation("Loading authentication data from {File}", Settings.CredentialsFile); // credentials if (File.Exists(Settings.CredentialsFile)) { @@ -87,6 +90,8 @@ public sealed class AuthStore : IDisposable foreach (var ip in arr) BlacklistIPs.Add(ip); } + _logger.LogInformation("Loaded {UserCount} users, {FpCount} fingerprints, {IpCount} IPs", + Credentials.Count, Fingerprints.Count, BlacklistIPs.Count); } #endregion @@ -106,7 +111,10 @@ public sealed class AuthStore : IDisposable private void ReloadCredentials() { if (!File.Exists(Settings.CredentialsFile)) + { + _logger.LogError("Credentials file {File} does not exist", Settings.CredentialsFile); return; + } try { @@ -140,8 +148,9 @@ public sealed class AuthStore : IDisposable } } } - catch + catch(Exception ex) { + _logger.LogError(ex, "Failed to reload credentials from {File}", Settings.CredentialsFile); // ignore – malformed JSON or IO error – keep old state } } @@ -167,6 +176,7 @@ public sealed class AuthStore : IDisposable cred = new Credential(username, PasswordHash: ComputeHash(password), 1, Verified: false); Credentials[username] = cred; PersistCredentials(); + _logger.LogInformation("Created new user {Username} (verified={Verified})", username, cred.Verified); var list = Fingerprints.GetOrAdd(username, _ => new List()); if (uid.HasValue && !list.Contains(uid.Value)) @@ -185,6 +195,7 @@ public sealed class AuthStore : IDisposable { error = "Invalid password"; IncrementFailed(username, ip); + _logger.LogWarning("Auth failed for {Username} from {IP} – {Error}", username, ip, error); return false; } @@ -224,13 +235,14 @@ public sealed class AuthStore : IDisposable { var newCount = FailedAttempts.GetOrAdd(username, 0) + 1; FailedAttempts[username] = newCount; + _logger.LogInformation("Failed attempts for {Username} increased to {Count}", username, newCount); - if (newCount >= Settings.MaxFailedAttempts) - { - BlacklistIPs.Add(ip); - PersistBlacklist(); - FailedAttempts[username] = 0; - } + if (newCount < Settings.MaxFailedAttempts) return; + + BlacklistIPs.Add(ip); + PersistBlacklist(); + FailedAttempts[username] = 0; + _logger.LogWarning("IP {IP} blacklisted after {Count} failures", ip, newCount); } #endregion diff --git a/TinfoilVibeServer/Middleware/BasicAuthMiddleware.cs b/TinfoilVibeServer/Middleware/BasicAuthMiddleware.cs index 1c59bc4..337c663 100644 --- a/TinfoilVibeServer/Middleware/BasicAuthMiddleware.cs +++ b/TinfoilVibeServer/Middleware/BasicAuthMiddleware.cs @@ -17,6 +17,7 @@ public sealed class BasicAuthMiddleware public async Task InvokeAsync(HttpContext context, AuthStore store, ILogger logger) { + logger.LogInformation("Incoming request from {IP} – {Method} {Path}", context.Connection.RemoteIpAddress, context.Request.Method, context.Request.Path); var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; // 1) IP blacklist @@ -31,7 +32,9 @@ public sealed class BasicAuthMiddleware // 2) Authorization header if (!context.Request.Headers.TryGetValue("Authorization", out var authHeaders)) { + logger.LogWarning("Missing Authorization header from {IP}", ip); Challenge(context); + logger.LogInformation("Sent 401 challenge to client"); return; } @@ -39,6 +42,7 @@ public sealed class BasicAuthMiddleware if (!authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) { Challenge(context); + logger.LogInformation("Sent 401 challenge to client"); return; } @@ -51,6 +55,7 @@ public sealed class BasicAuthMiddleware catch { Challenge(context); + logger.LogInformation("Sent 401 challenge to client"); return; } @@ -58,6 +63,7 @@ public sealed class BasicAuthMiddleware if (parts.Length != 2) { Challenge(context); + logger.LogInformation("Sent 401 challenge to client"); return; } @@ -84,7 +90,7 @@ public sealed class BasicAuthMiddleware // Authentication succeeded – attach username for downstream handlers if needed context.Items["User"] = username; - + logger.LogInformation("User {User} authenticated successfully (UID={UID})", username, uid); await _next(context); } diff --git a/TinfoilVibeServer/Program.cs b/TinfoilVibeServer/Program.cs index 2281f40..a801d7a 100644 --- a/TinfoilVibeServer/Program.cs +++ b/TinfoilVibeServer/Program.cs @@ -4,7 +4,9 @@ using TinfoilVibeServer.Services; using TinfoilVibeServer.Models; var builder = WebApplication.CreateBuilder(args); - +builder.Logging.ClearProviders(); +builder.Logging.AddConsole(); +builder.Logging.AddDebug(); // ------------------------------------------------------------------- // 1) Configuration – read appsettings.json once and expose it via // ConfigManager (reloads on file change) @@ -15,11 +17,13 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => { var config = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); var keySet = KeySetHolder.KeySet; // already loaded by ConfigManager - return new NSPExtractor(keySet); + return new NSPExtractor(keySet, logger); }); builder.Services.AddSingleton(); -builder.Services.AddSingleton(); +builder.Services.AddSingleton(sp => + new AuthStore(sp.GetRequiredService>())); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -43,7 +47,19 @@ app.MapControllers(); // routes the /index.json & /download endpoints app.MapGet("/debug", () => new SnapshotService( app.Services.GetRequiredService(), app.Services.GetRequiredService(), - app.Services.GetRequiredService()) + app.Services.GetRequiredService(), + app.Services.GetRequiredService>()) .GetSnapshot()); - +app.Lifetime.ApplicationStarted.Register(() => + app.Services.GetRequiredService>().LogInformation("Application started. Listening on {Urls}", string.Join(", ", app.Urls))); +app.Use(async (ctx, next) => +{ + var logger = app.Services.GetRequiredService>(); + var correlationId = ctx.Request.Headers["X-Correlation-ID"].FirstOrDefault() + ?? Guid.NewGuid().ToString(); + using (logger.BeginScope("CorrelationId:{CorrelationId}", correlationId)) + { + await next(); + } +}); app.Run(); \ No newline at end of file diff --git a/TinfoilVibeServer/Services/ArchiveHandler.cs b/TinfoilVibeServer/Services/ArchiveHandler.cs index f4b22fc..4202f7f 100644 --- a/TinfoilVibeServer/Services/ArchiveHandler.cs +++ b/TinfoilVibeServer/Services/ArchiveHandler.cs @@ -16,10 +16,12 @@ namespace TinfoilVibeServer.Services; public sealed class ArchiveHandler { private readonly NSPExtractor _nspExtractor; + private readonly ILogger _logger; - public ArchiveHandler(NSPExtractor nspExtractor) + public ArchiveHandler(NSPExtractor nspExtractor, ILogger logger) { _nspExtractor = nspExtractor; + _logger = logger; } /// @@ -27,20 +29,29 @@ public sealed class ArchiveHandler /// public NcaMetadataWithHash? TryExtractTitleInfo(string filePath) { + _logger.LogInformation("Examining archive {File} for embedded NSP", filePath); var ext = Path.GetExtension(filePath).ToLowerInvariant(); try { - return ext switch + switch (ext) { - ".zip" => HandleZip(filePath), - ".7z" => Handle7z(filePath), - ".rar" => HandleRar(filePath), - _ => null - }; + case ".zip": + return HandleZip(filePath); + case ".7z": + return Handle7z(filePath); + case ".rar": + return HandleRar(filePath); + default: + { + _logger.LogWarning("Unsupported archive type {Extension} – skipping", ext); + return null; + } + } } - catch + catch (Exception ex) { + _logger.LogError("Error opening archive {File}: {Exception}", filePath, ex.Message); // Graceful fallback – return null return null; } diff --git a/TinfoilVibeServer/Services/IndexBuilderService.cs b/TinfoilVibeServer/Services/IndexBuilderService.cs index d73cc2c..26b26d9 100644 --- a/TinfoilVibeServer/Services/IndexBuilderService.cs +++ b/TinfoilVibeServer/Services/IndexBuilderService.cs @@ -46,10 +46,11 @@ public sealed class IndexBuilderService: IHostedService string snapshotHash = ComputeSnapshotHash(snapshot); if (cached != null && cached.SnapshotHash == snapshotHash) { - _logger.LogInformation("Index cache is up‑to‑date – re‑using it."); + _logger.LogInformation("Using cached index (snapshot hash={Hash}", snapshotHash); return cached.Index; } + _logger.LogInformation("Building index (snapshot size={Count})", snapshot.Count); // 3️⃣ Build new index from snapshot entries var files = snapshot .Where(e => e.Title != null) @@ -85,6 +86,7 @@ public sealed class IndexBuilderService: IHostedService { if (!File.Exists(_cachePath)) return null; var json = File.ReadAllText(_cachePath); + _logger.LogInformation("Loaded index cache from {Path}", _cachePath); return JsonSerializer.Deserialize(json); } diff --git a/TinfoilVibeServer/Services/NSPExtractor.cs b/TinfoilVibeServer/Services/NSPExtractor.cs index 17de19a..5d4ca52 100644 --- a/TinfoilVibeServer/Services/NSPExtractor.cs +++ b/TinfoilVibeServer/Services/NSPExtractor.cs @@ -24,10 +24,12 @@ namespace TinfoilVibeServer.Services public sealed class NSPExtractor { private readonly KeySet _keySet; + private readonly ILogger _logger; - public NSPExtractor(KeySet keySet) + public NSPExtractor(KeySet keySet, ILogger logger) { _keySet = keySet; + _logger = logger; } /// @@ -44,6 +46,7 @@ namespace TinfoilVibeServer.Services /// public NcaMetadataWithHash? ExtractFromStream(Stream stream) { + _logger.LogInformation("Extracting NSP from stream (length={Length}", stream.Length); if (!IsPfs0FileSystem(stream)) return null; @@ -69,7 +72,8 @@ namespace TinfoilVibeServer.Services var nca = new Nca(_keySet, ncaFileStorage); if (nca.Header.ContentType != NcaContentType.Meta) continue; // only the meta NCA contains title metadata - + + _logger.LogInformation("Meta NCA found – TitleId={TitleId} Version={Version}", nca.Header.TitleId, nca.Header.Version); string titleId = nca.Header.TitleId.ToString("X16"); int version = nca.Header.Version; bool isPatch = nca.IsPatch; @@ -121,8 +125,9 @@ namespace TinfoilVibeServer.Services partition.Initialize(storage).ThrowIfFailure(); return true; } - catch + catch (Exception e) { + _logger.LogError("Failed to extract NSP: {Exception}", e.Message); return false; } } @@ -130,7 +135,7 @@ namespace TinfoilVibeServer.Services public string ExtractHashFromStream(Stream nspStream) { if (!IsPfs0FileSystem(nspStream)) - return null; + return string.Empty; nspStream.Seek(0, SeekOrigin.Begin); @@ -151,20 +156,25 @@ namespace TinfoilVibeServer.Services using var ncaFile = fileRef.Release(); using var ncaFileStorage = new FileStorage(ncaFile); - var nca = new Nca(_keySet, ncaFileStorage); - if (nca.Header.ContentType != NcaContentType.Meta) - continue; // only the meta NCA contains title metadata + try + { + var nca = new Nca(_keySet, ncaFileStorage); + if (nca.Header.ContentType != NcaContentType.Meta) + continue; // only the meta NCA contains title metadata - string titleId = nca.Header.TitleId.ToString("X16"); - int version = nca.Header.Version; - bool isPatch = nca.IsPatch; - bool isApp = nca.IsProgram && !isPatch; - - // Hash the *first* NCA stream – the stream we just opened - using var ncaStream = ncaFile.AsStream(); - using var sha256 = SHA256.Create(); - var hash = sha256.ComputeHash(ncaStream); - return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + // Hash the *first* NCA stream – the stream we just opened + using var ncaStream = ncaFile.AsStream(); + using var sha256 = SHA256.Create(); + var hash = sha256.ComputeHash(ncaStream); + var extractHashFromStream = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + _logger.LogInformation("Computed first‑stream hash {Hash} for {TitleId}", extractHashFromStream, + nca.Header.TitleId); + return extractHashFromStream; + } + catch (Exception e) + { + _logger.LogError("Failed to extract NSP: {Exception}", e.Message); + } } return string.Empty; } diff --git a/TinfoilVibeServer/Services/SnapshotService.cs b/TinfoilVibeServer/Services/SnapshotService.cs index 6ae6a1c..d3d4dd3 100644 --- a/TinfoilVibeServer/Services/SnapshotService.cs +++ b/TinfoilVibeServer/Services/SnapshotService.cs @@ -20,6 +20,7 @@ public sealed class SnapshotService : IDisposable, ISnapshotService private readonly ConfigManager _config; private readonly NSPExtractor _nspExtractor; private readonly ArchiveHandler _archiveHandler; + private readonly ILogger _logger; private readonly string _jsonPath; private readonly string _snapshotPath; private readonly List _watchers = new(); @@ -27,11 +28,12 @@ public sealed class SnapshotService : IDisposable, ISnapshotService private string? _currentSnapshotHash; public event EventHandler? SnapshotRebuilt; - public SnapshotService(ConfigManager config, NSPExtractor nspExtractor, ArchiveHandler archiveHandler) + public SnapshotService(ConfigManager config, NSPExtractor nspExtractor, ArchiveHandler archiveHandler, ILogger logger) { _config = config; _nspExtractor = nspExtractor; _archiveHandler = archiveHandler; + _logger = logger; _jsonPath = Path.Combine(AppContext.BaseDirectory, _config.Settings.SnapshotFile); _snapshotPath = Path.Combine(AppContext.BaseDirectory, _config.Settings.SnapshotBackupFile); @@ -70,35 +72,34 @@ public sealed class SnapshotService : IDisposable, ISnapshotService private void InitializeFileSystemWatcher(string path) { - if (Directory.Exists(path)) + if (!Directory.Exists(path)) return; + var watcher = new FileSystemWatcher { - var _watcher = new FileSystemWatcher - { - Path = path, - IncludeSubdirectories = true, - NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | - NotifyFilters.Size | NotifyFilters.LastWrite - }; - _watcher.Created += OnChanged; - _watcher.Changed += OnChanged; - _watcher.Deleted += OnChanged; - _watcher.Renamed += OnRenamed; - _watcher.EnableRaisingEvents = true; + Path = path, + IncludeSubdirectories = true, + NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | + NotifyFilters.Size | NotifyFilters.LastWrite + }; + watcher.Created += OnChanged; + watcher.Changed += OnChanged; + watcher.Deleted += OnChanged; + watcher.Renamed += OnRenamed; + watcher.EnableRaisingEvents = true; - _watchers.Add(_watcher); - } + _watchers.Add(watcher); } #region FileSystemWatcher - private void OnChanged(object? _, FileSystemEventArgs e) => ThrottleSnapshotUpdate(); - private void OnRenamed(object? _, RenamedEventArgs e) => ThrottleSnapshotUpdate(); + private void OnChanged(object? _, FileSystemEventArgs e) => ThrottleSnapshotUpdate(e); + private void OnRenamed(object? _, RenamedEventArgs e) => ThrottleSnapshotUpdate(e); - private void ThrottleSnapshotUpdate() + private void ThrottleSnapshotUpdate(FileSystemEventArgs fileSystemEventArgs) { Task.Run(async () => { await Task.Delay(250); + _logger.LogDebug("File system event {EventType} on {Path}", fileSystemEventArgs.ChangeType, fileSystemEventArgs.FullPath); UpdateSnapshot(); }); } @@ -110,8 +111,9 @@ public sealed class SnapshotService : IDisposable, ISnapshotService private void BuildSnapshot() { var cfg = _config.Settings; + _logger.LogInformation("Rebuilding snapshot (root dirs: {Count})", cfg.RootDirectories.Length); var entries = new List(); - var index = LoadSnapshotIndex(); // <‑ new + var index = LoadSnapshotIndex(); var snapshotChanged = false; foreach (var dir in cfg.RootDirectories) @@ -149,11 +151,16 @@ public sealed class SnapshotService : IDisposable, ISnapshotService title = _archiveHandler.TryExtractTitleInfo(file); } + if (title == null) + { + _logger.LogInformation("Failed to process {File}", file); + } // 4) update cache _cache[file] = new CachedFile(file, hash, title); // 5) add to snapshot entries.Add(new FileEntry(file, new FileInfo(file).Length, hash, title)); + _logger.LogInformation("Added {File} to snapshot (hash={Hash}", file, hash); snapshotChanged = true; } } @@ -180,6 +187,7 @@ public sealed class SnapshotService : IDisposable, ISnapshotService var newHash = ComputeSnapshotHash(entries); if (_currentSnapshotHash != newHash) { + _logger.LogInformation("Snapshot hash changed – persisting new snapshot"); _currentSnapshotHash = newHash; File.WriteAllText(_jsonPath, JsonSerializer.Serialize(entries)); File.WriteAllText(_snapshotPath, JsonSerializer.Serialize(entries)); diff --git a/TinfoilVibeServer/Services/TitleDatabaseService.cs b/TinfoilVibeServer/Services/TitleDatabaseService.cs index 0f4096b..0c08766 100644 --- a/TinfoilVibeServer/Services/TitleDatabaseService.cs +++ b/TinfoilVibeServer/Services/TitleDatabaseService.cs @@ -30,6 +30,8 @@ public sealed class TitleDatabaseService : IHostedService private readonly IMemoryCache _cache; private readonly ISnapshotService _snapshotService; + + private readonly Dictionary _titleIdToPath = new Dictionary(); // 1️⃣ Cache for the JSON data (key = TitleId) /* private readonly ConcurrentDictionary _titleData @@ -90,9 +92,6 @@ public sealed class TitleDatabaseService : IHostedService // 1️⃣ Load the JSON (download if not cached). await ReloadCacheAsync(); - // 2️⃣ Scan the file‑system and build the title‑id → path map. - BuildFilesystemIndex(); - _logger.LogInformation("Title database ready – {Count} entries loaded.", GetAllAsync().Result.Count); } @@ -107,7 +106,9 @@ public sealed class TitleDatabaseService : IHostedService /* ---------------------------------------------------------------- */ private async Task ReloadCacheAsync() { - var ttlSec = _options.CurrentValue.TtlSeconds; + var ttlSec = _options.CurrentValue.TtlSeconds; + _logger.LogInformation("Reloading title database cache (TTL={TTL}s).", ttlSec); + var entryOptions = new MemoryCacheEntryOptions() .SetSlidingExpiration(TimeSpan.FromSeconds(ttlSec)).RegisterPostEvictionCallback((key, value, reason, state) => @@ -130,6 +131,7 @@ public sealed class TitleDatabaseService : IHostedService var cacheFile = Path.Combine(_cacheFolder, $"{_options.CurrentValue.CountryCode}.{_options.CurrentValue.LanguageCode}.json"); if (!File.Exists(cacheFile)) { + _logger.LogInformation("Cache miss – downloading title DB from {Url}", cacheFile); await LoadAndCacheTitleDb(CancellationToken.None); } @@ -222,6 +224,7 @@ public sealed class TitleDatabaseService : IHostedService await LoadAndCacheTitleDb(ct); return; } + _logger.LogInformation("Cache miss – downloading title DB from {Url}", cacheFile); } _logger.LogInformation("Downloading title database from {Url}", rawUrl); @@ -271,47 +274,5 @@ public sealed class TitleDatabaseService : IHostedService } return titleData; } - - /// - /// Scan the configured root directories and create the - /// _titleIdToPath map. If the file name does not contain a - /// TitleId, we extract it from the NSP file. - /// - private void BuildFilesystemIndex() - { - foreach (var root in _rootDirectories) - { - if (!Directory.Exists(root)) continue; - - foreach (var file in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories)) - { - if (file.EndsWith(".nsp", StringComparison.OrdinalIgnoreCase)) - { - // 1️⃣ Does the file name already contain a TitleId? - var match = _titleIdRegex.Match(Path.GetFileName(file)); - string? titleId; - if (match.Success) - { - titleId = match.Groups[1].Value + match.Groups[2].Value; - } - else - { - // 2️⃣ Extract the TitleId from the NSP using the extractor. - titleId = _nspExtractor.ExtractFromStream(File.OpenRead(file))?.TitleId; - if (string.IsNullOrWhiteSpace(titleId)) - { - _logger.LogWarning("Could not extract TitleId from {File}", file); - continue; - } - } - - // Normalise to 16‑digit hex (upper‑case). - titleId = titleId.ToUpperInvariant(); - //_titleIdToPath[titleId] = file; - } - } - } - } - #endregion } \ No newline at end of file