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