Enhanced logging & error handling for auth and pg backups
- Add detailed debug/warning logs to authentication flows (AuthService, AuthorizationContext, WebSocketManager) for better traceability of auth attempts and failures. - Log WebSocket authentication attempts, including IP, path, and token preview. - Introduce PostgresVersionMismatchException for pg_dump/pg_restore version mismatches; parse and log server/client versions, clean up failed backups, and provide upgrade guidance. - Add helper for parsing version mismatch errors. - Update using directives and minor code cleanups. - Update .csproj.user with new publish profile path.
This commit is contained in:
@@ -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<AuthService> _logger;
|
||||
|
||||
public AuthService(
|
||||
IAuthorizationContext authorizationContext)
|
||||
IAuthorizationContext authorizationContext,
|
||||
ILogger<AuthService> logger)
|
||||
{
|
||||
_authorizationContext = authorizationContext;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<AuthorizationInfo> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,23 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <inheritdoc />
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<AuthorizationContext> _logger;
|
||||
|
||||
public AuthorizationContext(
|
||||
IDbContextFactory<JellyfinDbContext> jellyfinDb,
|
||||
IUserManager userManager,
|
||||
IDeviceManager deviceManager,
|
||||
IServerApplicationHost serverApplicationHost,
|
||||
IServerConfigurationManager configurationManager)
|
||||
IServerConfigurationManager configurationManager,
|
||||
ILogger<AuthorizationContext> logger)
|
||||
{
|
||||
_jellyfinDbProvider = jellyfinDb;
|
||||
_userManager = userManager;
|
||||
_deviceManager = deviceManager;
|
||||
_serverApplicationHost = serverApplicationHost;
|
||||
_configurationManager = configurationManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<AuthorizationInfo> 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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<NameOfLastUsedPublishProfile>E:\Projects\pgsql-jellyfin\Jellyfin.Server\Properties\PublishProfiles\FolderProfile.pubxml</NameOfLastUsedPublishProfile>
|
||||
<NameOfLastUsedPublishProfile>D:\Projects\pgsql-jellyfin\Jellyfin.Server\Properties\PublishProfiles\FolderProfile.pubxml</NameOfLastUsedPublishProfile>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
// <copyright file="PostgresVersionMismatchException.cs" company="PlaceholderCompany">
|
||||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Jellyfin.Database.Providers.Postgres.Exceptions;
|
||||
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when there is a version mismatch between PostgreSQL server and client tools (pg_dump/pg_restore).
|
||||
/// </summary>
|
||||
public class PostgresVersionMismatchException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PostgresVersionMismatchException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverVersion">The PostgreSQL server version.</param>
|
||||
/// <param name="clientVersion">The pg_dump/pg_restore client version.</param>
|
||||
public PostgresVersionMismatchException(string serverVersion, string clientVersion)
|
||||
: base(BuildMessage(serverVersion, clientVersion))
|
||||
{
|
||||
ServerVersion = serverVersion;
|
||||
ClientVersion = clientVersion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PostgresVersionMismatchException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverVersion">The PostgreSQL server version.</param>
|
||||
/// <param name="clientVersion">The pg_dump/pg_restore client version.</param>
|
||||
/// <param name="innerException">The inner exception.</param>
|
||||
public PostgresVersionMismatchException(string serverVersion, string clientVersion, Exception innerException)
|
||||
: base(BuildMessage(serverVersion, clientVersion), innerException)
|
||||
{
|
||||
ServerVersion = serverVersion;
|
||||
ClientVersion = clientVersion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the PostgreSQL server version.
|
||||
/// </summary>
|
||||
public string ServerVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pg_dump/pg_restore client version.
|
||||
/// </summary>
|
||||
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.";
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses PostgreSQL version mismatch error message to extract server and client versions.
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">The error message from pg_dump.</param>
|
||||
/// <returns>A tuple containing server version and client version.</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user