diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index f6fa7509..731ca19f 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -4,22 +4,27 @@ #pragma warning disable CS1591 +using System; using System.Threading.Tasks; using Jellyfin.Data; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.HttpServer.Security { public class AuthService : IAuthService { private readonly IAuthorizationContext _authorizationContext; + private readonly ILogger _logger; public AuthService( - IAuthorizationContext authorizationContext) + IAuthorizationContext authorizationContext, + ILogger logger) { _authorizationContext = authorizationContext; + _logger = logger; } public async Task Authenticate(HttpRequest request) @@ -28,19 +33,44 @@ namespace Emby.Server.Implementations.HttpServer.Security if (!auth.HasToken) { + _logger.LogDebug( + "Authentication failed: No token provided. Path: {Path}, Client: {Client}, Device: {Device}, DeviceId: {DeviceId}", + request.Path, + auth.Client ?? "unknown", + auth.Device ?? "unknown", + auth.DeviceId ?? "unknown"); return auth; } if (!auth.IsAuthenticated) { + var tokenPreview = auth.Token?.Length > 8 ? auth.Token.Substring(0, 8) + "..." : auth.Token ?? "null"; + _logger.LogDebug( + "Authentication failed: Invalid token. Path: {Path}, Client: {Client}, Device: {Device}, DeviceId: {DeviceId}, Token: {Token}", + request.Path, + auth.Client ?? "unknown", + auth.Device ?? "unknown", + auth.DeviceId ?? "unknown", + tokenPreview); throw new SecurityException("Invalid token."); } if (auth.User?.HasPermission(PermissionKind.IsDisabled) ?? false) { + _logger.LogWarning( + "Authentication failed: User account disabled. User: {User}, Path: {Path}", + auth.User.Username, + request.Path); throw new SecurityException("User account has been disabled."); } + _logger.LogDebug( + "Authentication successful. User: {User}, Client: {Client}, Device: {Device}, Path: {Path}", + auth.User?.Username ?? "ApiKey", + auth.Client ?? "unknown", + auth.Device ?? "unknown", + request.Path); + return auth; } } diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs index 879d960b..4d859140 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs @@ -40,9 +40,23 @@ namespace Emby.Server.Implementations.HttpServer /// public async Task WebSocketRequestHandler(HttpContext context) { + _logger.LogDebug( + "WebSocket authentication attempt from {IP}, Path: {Path}, QueryString: {QueryString}", + context.Connection.RemoteIpAddress, + context.Request.Path, + context.Request.QueryString); + var authorizationInfo = await _authService.Authenticate(context.Request).ConfigureAwait(false); if (!authorizationInfo.IsAuthenticated) { + var tokenPreview = authorizationInfo.HasToken && authorizationInfo.Token?.Length > 8 + ? authorizationInfo.Token.Substring(0, 8) + "..." + : authorizationInfo.Token ?? "null"; + _logger.LogDebug( + "WebSocket authentication failed from {IP}. HasToken: {HasToken}, Token: {Token}", + context.Connection.RemoteIpAddress, + authorizationInfo.HasToken, + tokenPreview); throw new SecurityException("Token is required"); } diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index a222973c..c5da6b59 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Jellyfin.Data; using Jellyfin.Data.Enums; @@ -18,6 +19,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.TV; using MediaBrowser.Model.Querying; +using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; using Series = MediaBrowser.Controller.Entities.TV.Series; diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index ef78feb5..0276f7e7 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -18,6 +18,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; namespace Jellyfin.Server.Implementations.Security @@ -29,19 +30,22 @@ namespace Jellyfin.Server.Implementations.Security private readonly IDeviceManager _deviceManager; private readonly IServerApplicationHost _serverApplicationHost; private readonly IServerConfigurationManager _configurationManager; + private readonly ILogger _logger; public AuthorizationContext( IDbContextFactory jellyfinDb, IUserManager userManager, IDeviceManager deviceManager, IServerApplicationHost serverApplicationHost, - IServerConfigurationManager configurationManager) + IServerConfigurationManager configurationManager, + ILogger logger) { _jellyfinDbProvider = jellyfinDb; _userManager = userManager; _deviceManager = deviceManager; _serverApplicationHost = serverApplicationHost; _configurationManager = configurationManager; + _logger = logger; } public Task GetAuthorizationInfo(HttpContext requestContext) @@ -127,9 +131,23 @@ namespace Jellyfin.Server.Implementations.Security if (!authInfo.HasToken) { // Request doesn't contain a token. + _logger.LogDebug( + "No authentication token found. Client: {Client}, Device: {Device}, DeviceId: {DeviceId}, HasAuthHeader: {HasAuthHeader}", + client ?? "unknown", + deviceName ?? "unknown", + deviceId ?? "unknown", + auth is not null); return authInfo; } + var tokenPreview = token?.Length > 8 ? token.Substring(0, 8) + "..." : token ?? "null"; + _logger.LogDebug( + "Validating authentication token. Client: {Client}, Device: {Device}, DeviceId: {DeviceId}, Token: {Token}", + client ?? "unknown", + deviceName ?? "unknown", + deviceId ?? "unknown", + tokenPreview); + var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { @@ -138,6 +156,10 @@ namespace Jellyfin.Server.Implementations.Security if (device is not null) { + _logger.LogDebug( + "Token validated against device. Device: {DeviceName}, User: {UserId}", + device.DeviceName, + device.UserId); authInfo.IsAuthenticated = true; var updateToken = false; @@ -199,6 +221,9 @@ namespace Jellyfin.Server.Implementations.Security var key = await dbContext.ApiKeys.FirstOrDefaultAsync(apiKey => apiKey.AccessToken == token).ConfigureAwait(false); if (key is not null) { + _logger.LogDebug( + "Token validated against API key. ApiKey: {ApiKeyName}", + key.Name); authInfo.IsAuthenticated = true; authInfo.Client = key.Name; authInfo.Token = key.AccessToken; @@ -219,6 +244,13 @@ namespace Jellyfin.Server.Implementations.Security authInfo.IsApiKey = true; } + else + { + var tokenPreview2 = token?.Length > 8 ? token.Substring(0, 8) + "..." : token ?? "null"; + _logger.LogDebug( + "Token validation failed: Token not found in devices or API keys. Token: {Token}", + tokenPreview2); + } } return authInfo; diff --git a/Jellyfin.Server/Jellyfin.Server.csproj.user b/Jellyfin.Server/Jellyfin.Server.csproj.user index d245b4ec..a8c73ec9 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj.user +++ b/Jellyfin.Server/Jellyfin.Server.csproj.user @@ -1,6 +1,6 @@  - E:\Projects\pgsql-jellyfin\Jellyfin.Server\Properties\PublishProfiles\FolderProfile.pubxml + D:\Projects\pgsql-jellyfin\Jellyfin.Server\Properties\PublishProfiles\FolderProfile.pubxml \ No newline at end of file diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Exceptions/PostgresVersionMismatchException.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Exceptions/PostgresVersionMismatchException.cs new file mode 100644 index 00000000..355725e9 --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Exceptions/PostgresVersionMismatchException.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace Jellyfin.Database.Providers.Postgres.Exceptions; + +using System; + +/// +/// Exception thrown when there is a version mismatch between PostgreSQL server and client tools (pg_dump/pg_restore). +/// +public class PostgresVersionMismatchException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// The PostgreSQL server version. + /// The pg_dump/pg_restore client version. + public PostgresVersionMismatchException(string serverVersion, string clientVersion) + : base(BuildMessage(serverVersion, clientVersion)) + { + ServerVersion = serverVersion; + ClientVersion = clientVersion; + } + + /// + /// Initializes a new instance of the class. + /// + /// The PostgreSQL server version. + /// The pg_dump/pg_restore client version. + /// The inner exception. + public PostgresVersionMismatchException(string serverVersion, string clientVersion, Exception innerException) + : base(BuildMessage(serverVersion, clientVersion), innerException) + { + ServerVersion = serverVersion; + ClientVersion = clientVersion; + } + + /// + /// Gets the PostgreSQL server version. + /// + public string ServerVersion { get; } + + /// + /// Gets the pg_dump/pg_restore client version. + /// + public string ClientVersion { get; } + + private static string BuildMessage(string serverVersion, string clientVersion) + { + return $"PostgreSQL server version ({serverVersion}) does not match pg_dump version ({clientVersion}). " + + "Please upgrade your PostgreSQL client tools to match or exceed the server version. " + + "You can specify a custom path to pg_dump in the database backup options configuration."; + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresBackupService.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresBackupService.cs index 9a80eecc..420f5668 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresBackupService.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresBackupService.cs @@ -10,9 +10,11 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Jellyfin.Database.Implementations.DbConfiguration; +using Jellyfin.Database.Providers.Postgres.Exceptions; using MediaBrowser.Common.Configuration; using Microsoft.Extensions.Logging; @@ -137,6 +139,26 @@ public class PostgresBackupService var errorMessage = errorBuilder.ToString(); _logger.LogError("pg_dump failed with exit code {ExitCode}: {Error}", process.ExitCode, errorMessage); + // Check for version mismatch error + if (errorMessage.Contains("server version mismatch", StringComparison.OrdinalIgnoreCase)) + { + var (serverVersion, clientVersion) = ParseVersionMismatchError(errorMessage); + + _logger.LogError( + "PostgreSQL version mismatch detected - Server: {ServerVersion}, pg_dump: {ClientVersion}. " + + "Please upgrade your PostgreSQL client tools to match the server version.", + serverVersion, + clientVersion); + + // Clean up failed backup file + if (File.Exists(fullBackupPath)) + { + File.Delete(fullBackupPath); + } + + throw new PostgresVersionMismatchException(serverVersion, clientVersion); + } + // Clean up failed backup file if (File.Exists(fullBackupPath)) { @@ -478,4 +500,31 @@ public class PostgresBackupService _ => "dump" }; } + + /// + /// Parses PostgreSQL version mismatch error message to extract server and client versions. + /// + /// The error message from pg_dump. + /// A tuple containing server version and client version. + private static (string ServerVersion, string ClientVersion) ParseVersionMismatchError(string errorMessage) + { + var serverVersion = "unknown"; + var clientVersion = "unknown"; + + // Parse server version: "server version: 18.3 (Debian 18.3-1.pgdg13+1)" + var serverMatch = Regex.Match(errorMessage, @"server version:\s*([^\s;]+(?:\s*\([^)]+\))?)", RegexOptions.IgnoreCase); + if (serverMatch.Success) + { + serverVersion = serverMatch.Groups[1].Value.Trim(); + } + + // Parse pg_dump version: "pg_dump version: 16.12 (Ubuntu 16.12-1.pgdg24.04+1)" + var clientMatch = Regex.Match(errorMessage, @"pg_dump version:\s*([^\s;]+(?:\s*\([^)]+\))?)", RegexOptions.IgnoreCase); + if (clientMatch.Success) + { + clientVersion = clientMatch.Groups[1].Value.Trim(); + } + + return (serverVersion, clientVersion); + } } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresDatabaseProvider.cs index bd7421be..9d3a4838 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresDatabaseProvider.cs @@ -18,6 +18,7 @@ using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.DbConfiguration; using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Entities.Security; +using Jellyfin.Database.Providers.Postgres.Exceptions; using MediaBrowser.Common.Configuration; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -715,6 +716,17 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider logger.LogInformation("PostgreSQL local backup created: {BackupPath}", backupPath); return Path.GetFileName(backupPath); } + catch (PostgresVersionMismatchException versionEx) + { + logger.LogError( + versionEx, + "PostgreSQL version mismatch: Server version {ServerVersion} is incompatible with pg_dump version {ClientVersion}. " + + "Please upgrade your PostgreSQL client tools (pg_dump/pg_restore) to match or exceed the server version. " + + "You can install the latest PostgreSQL client tools from: https://www.postgresql.org/download/", + versionEx.ServerVersion, + versionEx.ClientVersion); + throw; + } catch (Exception ex) { logger.LogError(ex, "Failed to create local PostgreSQL backup using pg_dump");