浏览代码

Merge pull request #9875 from Shadowghost/fixes

Bond-009 1 年之前
父节点
当前提交
07727e1d63

+ 12 - 16
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -12,6 +12,7 @@ using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net.WebSocketMessages;
 using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
 using MediaBrowser.Model.Session;
+using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.HttpServer
@@ -43,14 +44,17 @@ namespace Emby.Server.Implementations.HttpServer
         /// </summary>
         /// <param name="logger">The logger.</param>
         /// <param name="socket">The socket.</param>
+        /// <param name="authorizationInfo">The authorization information.</param>
         /// <param name="remoteEndPoint">The remote end point.</param>
         public WebSocketConnection(
             ILogger<WebSocketConnection> logger,
             WebSocket socket,
+            AuthorizationInfo authorizationInfo,
             IPAddress? remoteEndPoint)
         {
             _logger = logger;
             _socket = socket;
+            AuthorizationInfo = authorizationInfo;
             RemoteEndPoint = remoteEndPoint;
 
             _jsonOptions = JsonDefaults.Options;
@@ -60,30 +64,22 @@ namespace Emby.Server.Implementations.HttpServer
         /// <inheritdoc />
         public event EventHandler<EventArgs>? Closed;
 
-        /// <summary>
-        /// Gets the remote end point.
-        /// </summary>
+        /// <inheritdoc />
+        public AuthorizationInfo AuthorizationInfo { get; }
+
+        /// <inheritdoc />
         public IPAddress? RemoteEndPoint { get; }
 
-        /// <summary>
-        /// Gets or sets the receive action.
-        /// </summary>
-        /// <value>The receive action.</value>
+        /// <inheritdoc />
         public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
 
-        /// <summary>
-        /// Gets the last activity date.
-        /// </summary>
-        /// <value>The last activity date.</value>
+        /// <inheritdoc />
         public DateTime LastActivityDate { get; private set; }
 
         /// <inheritdoc />
         public DateTime LastKeepAliveDate { get; set; }
 
-        /// <summary>
-        /// Gets the state.
-        /// </summary>
-        /// <value>The state.</value>
+        /// <inheritdoc />
         public WebSocketState State => _socket.State;
 
         /// <inheritdoc />
@@ -101,7 +97,7 @@ namespace Emby.Server.Implementations.HttpServer
         }
 
         /// <inheritdoc />
-        public async Task ProcessAsync(CancellationToken cancellationToken = default)
+        public async Task ReceiveAsync(CancellationToken cancellationToken = default)
         {
             var pipe = new Pipe();
             var writer = pipe.Writer;

+ 2 - 1
Emby.Server.Implementations/HttpServer/WebSocketManager.cs

@@ -51,6 +51,7 @@ namespace Emby.Server.Implementations.HttpServer
                 using var connection = new WebSocketConnection(
                     _loggerFactory.CreateLogger<WebSocketConnection>(),
                     webSocket,
+                    authorizationInfo,
                     context.GetNormalizedRemoteIP())
                 {
                     OnReceive = ProcessWebSocketMessageReceived
@@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.HttpServer
 
                 await Task.WhenAll(tasks).ConfigureAwait(false);
 
-                await connection.ProcessAsync().ConfigureAwait(false);
+                await connection.ReceiveAsync().ConfigureAwait(false);
                 _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
             }
             catch (Exception ex) // Otherwise ASP.Net will ignore the exception

+ 3 - 2
Emby.Server.Implementations/Session/SessionManager.cs

@@ -24,6 +24,7 @@ using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Authentication;
 using MediaBrowser.Controller.Events.Session;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
@@ -1462,7 +1463,7 @@ namespace Emby.Server.Implementations.Session
 
             if (user is null)
             {
-                await _eventManager.PublishAsync(new GenericEventArgs<AuthenticationRequest>(request)).ConfigureAwait(false);
+                await _eventManager.PublishAsync(new AuthenticationRequestEventArgs(request)).ConfigureAwait(false);
                 throw new AuthenticationException("Invalid username or password entered.");
             }
 
@@ -1498,7 +1499,7 @@ namespace Emby.Server.Implementations.Session
                 ServerId = _appHost.SystemId
             };
 
-            await _eventManager.PublishAsync(new GenericEventArgs<AuthenticationResult>(returnResult)).ConfigureAwait(false);
+            await _eventManager.PublishAsync(new AuthenticationResultEventArgs(returnResult)).ConfigureAwait(false);
             return returnResult;
         }
 

+ 17 - 1
Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs

@@ -1,6 +1,8 @@
 using System;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using Jellyfin.Data.Events;
+using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.Session;
@@ -9,7 +11,7 @@ using Microsoft.Extensions.Logging;
 namespace Jellyfin.Api.WebSocketListeners;
 
 /// <summary>
-/// Class SessionInfoWebSocketListener.
+/// Class ActivityLogWebSocketListener.
 /// </summary>
 public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<ActivityLogEntry[], WebSocketListenerState>
 {
@@ -56,6 +58,20 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
         base.Dispose(dispose);
     }
 
+    /// <summary>
+    /// Starts sending messages over an activity log web socket.
+    /// </summary>
+    /// <param name="message">The message.</param>
+    protected override void Start(WebSocketMessageInfo message)
+    {
+        if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
+        {
+            throw new AuthenticationException("Only admin users can retrieve the activity log.");
+        }
+
+        base.Start(message);
+    }
+
     private async void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
     {
         await SendData(true).ConfigureAwait(false);

+ 16 - 0
Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs

@@ -1,5 +1,7 @@
 using System.Collections.Generic;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
@@ -66,6 +68,20 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
         base.Dispose(dispose);
     }
 
+    /// <summary>
+    /// Starts sending messages over a session info web socket.
+    /// </summary>
+    /// <param name="message">The message.</param>
+    protected override void Start(WebSocketMessageInfo message)
+    {
+        if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
+        {
+            throw new AuthenticationException("Only admin users can subscribe to session information.");
+        }
+
+        base.Start(message);
+    }
+
     private async void OnSessionManagerSessionActivity(object? sender, SessionEventArgs e)
     {
         await SendData(false).ConfigureAwait(false);

+ 1 - 1
Jellyfin.Networking/Extensions/NetworkExtensions.cs

@@ -104,7 +104,7 @@ public static partial class NetworkExtensions
         Span<byte> bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? Network.IPv4MaskBytes : Network.IPv6MaskBytes];
         if (!mask.TryWriteBytes(bytes, out var bytesWritten))
         {
-            Console.WriteLine("Unable to write address bytes, only {bytesWritten} bytes written.");
+            Console.WriteLine("Unable to write address bytes, only ${bytesWritten} bytes written.");
         }
 
         var zeroed = false;

+ 5 - 6
Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs

@@ -2,9 +2,8 @@
 using System.Globalization;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
-using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Events;
-using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.Events.Authentication;
 using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.Globalization;
 using Microsoft.Extensions.Logging;
@@ -14,7 +13,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
     /// <summary>
     /// Creates an entry in the activity log when there is a failed login attempt.
     /// </summary>
-    public class AuthenticationFailedLogger : IEventConsumer<GenericEventArgs<AuthenticationRequest>>
+    public class AuthenticationFailedLogger : IEventConsumer<AuthenticationRequestEventArgs>
     {
         private readonly ILocalizationManager _localizationManager;
         private readonly IActivityManager _activityManager;
@@ -31,13 +30,13 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
         }
 
         /// <inheritdoc />
-        public async Task OnEvent(GenericEventArgs<AuthenticationRequest> eventArgs)
+        public async Task OnEvent(AuthenticationRequestEventArgs eventArgs)
         {
             await _activityManager.CreateAsync(new ActivityLog(
                 string.Format(
                     CultureInfo.InvariantCulture,
                     _localizationManager.GetLocalizedString("FailedLoginAttemptWithUserName"),
-                    eventArgs.Argument.Username),
+                    eventArgs.Username),
                 "AuthenticationFailed",
                 Guid.Empty)
             {
@@ -45,7 +44,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
                     _localizationManager.GetLocalizedString("LabelIpAddressValue"),
-                    eventArgs.Argument.RemoteEndPoint),
+                    eventArgs.RemoteEndPoint),
             }).ConfigureAwait(false);
         }
     }

+ 6 - 6
Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs

@@ -2,8 +2,8 @@
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Events;
-using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Authentication;
 using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.Globalization;
 
@@ -12,7 +12,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
     /// <summary>
     /// Creates an entry in the activity log when there is a successful login attempt.
     /// </summary>
-    public class AuthenticationSucceededLogger : IEventConsumer<GenericEventArgs<AuthenticationResult>>
+    public class AuthenticationSucceededLogger : IEventConsumer<AuthenticationResultEventArgs>
     {
         private readonly ILocalizationManager _localizationManager;
         private readonly IActivityManager _activityManager;
@@ -29,20 +29,20 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
         }
 
         /// <inheritdoc />
-        public async Task OnEvent(GenericEventArgs<AuthenticationResult> eventArgs)
+        public async Task OnEvent(AuthenticationResultEventArgs eventArgs)
         {
             await _activityManager.CreateAsync(new ActivityLog(
                 string.Format(
                     CultureInfo.InvariantCulture,
                     _localizationManager.GetLocalizedString("AuthenticationSucceededWithUserName"),
-                    eventArgs.Argument.User.Name),
+                    eventArgs.User.Name),
                 "AuthenticationSucceeded",
-                eventArgs.Argument.User.Id)
+                eventArgs.User.Id)
             {
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
                     _localizationManager.GetLocalizedString("LabelIpAddressValue"),
-                    eventArgs.Argument.SessionInfo.RemoteEndPoint),
+                    eventArgs.SessionInfo?.RemoteEndPoint),
             }).ConfigureAwait(false);
         }
     }

+ 12 - 9
Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs

@@ -58,15 +58,18 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
             var user = eventArgs.Users[0];
 
             await _activityManager.CreateAsync(new ActivityLog(
-                    string.Format(
-                        CultureInfo.InvariantCulture,
-                        _localizationManager.GetLocalizedString("UserStartedPlayingItemWithValues"),
-                        user.Username,
-                        GetItemName(eventArgs.MediaInfo),
-                        eventArgs.DeviceName),
-                    GetPlaybackNotificationType(eventArgs.MediaInfo.MediaType),
-                    user.Id))
-                .ConfigureAwait(false);
+                string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localizationManager.GetLocalizedString("UserStartedPlayingItemWithValues"),
+                    user.Username,
+                    GetItemName(eventArgs.MediaInfo),
+                    eventArgs.DeviceName),
+                GetPlaybackNotificationType(eventArgs.MediaInfo.MediaType),
+                user.Id)
+            {
+                ItemId = eventArgs.Item?.Id.ToString("N", CultureInfo.InvariantCulture),
+            })
+            .ConfigureAwait(false);
         }
 
         private static string GetItemName(BaseItemDto item)

+ 4 - 1
Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs

@@ -73,7 +73,10 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
                         GetItemName(item),
                         eventArgs.DeviceName),
                     notificationType,
-                    user.Id))
+                    user.Id)
+                {
+                    ItemId = eventArgs.Item?.Id.ToString("N", CultureInfo.InvariantCulture),
+                })
                 .ConfigureAwait(false);
         }
 

+ 3 - 4
Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs

@@ -8,12 +8,11 @@ using Jellyfin.Server.Implementations.Events.Consumers.System;
 using Jellyfin.Server.Implementations.Events.Consumers.Updates;
 using Jellyfin.Server.Implementations.Events.Consumers.Users;
 using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Authentication;
 using MediaBrowser.Controller.Events.Session;
 using MediaBrowser.Controller.Events.Updates;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.DependencyInjection;
@@ -35,8 +34,8 @@ namespace Jellyfin.Server.Implementations.Events
             collection.AddScoped<IEventConsumer<SubtitleDownloadFailureEventArgs>, SubtitleDownloadFailureLogger>();
 
             // Security consumers
-            collection.AddScoped<IEventConsumer<GenericEventArgs<AuthenticationRequest>>, AuthenticationFailedLogger>();
-            collection.AddScoped<IEventConsumer<GenericEventArgs<AuthenticationResult>>, AuthenticationSucceededLogger>();
+            collection.AddScoped<IEventConsumer<AuthenticationRequestEventArgs>, AuthenticationFailedLogger>();
+            collection.AddScoped<IEventConsumer<AuthenticationResultEventArgs>, AuthenticationSucceededLogger>();
 
             // Session consumers
             collection.AddScoped<IEventConsumer<PlaybackStartEventArgs>, PlaybackStartLogger>();

+ 60 - 0
MediaBrowser.Controller/Events/Authentication/AuthenticationRequestEventArgs.cs

@@ -0,0 +1,60 @@
+using System;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.Events.Authentication;
+
+/// <summary>
+/// A class representing an authentication result event.
+/// </summary>
+public class AuthenticationRequestEventArgs : EventArgs
+{
+    /// <summary>
+    /// Initializes a new instance of the <see cref="AuthenticationRequestEventArgs"/> class.
+    /// </summary>
+    /// <param name="request">The <see cref="AuthenticationRequest"/>.</param>
+    public AuthenticationRequestEventArgs(AuthenticationRequest request)
+    {
+        Username = request.Username;
+        UserId = request.UserId;
+        App = request.App;
+        AppVersion = request.AppVersion;
+        DeviceId = request.DeviceId;
+        DeviceName = request.DeviceName;
+        RemoteEndPoint = request.RemoteEndPoint;
+    }
+
+    /// <summary>
+    /// Gets or sets the user name.
+    /// </summary>
+    public string? Username { get; set; }
+
+    /// <summary>
+    /// Gets or sets the user id.
+    /// </summary>
+    public Guid? UserId { get; set; }
+
+    /// <summary>
+    /// Gets or sets the app.
+    /// </summary>
+    public string? App { get; set; }
+
+    /// <summary>
+    /// Gets or sets the app version.
+    /// </summary>
+    public string? AppVersion { get; set; }
+
+    /// <summary>
+    /// Gets or sets the device id.
+    /// </summary>
+    public string? DeviceId { get; set; }
+
+    /// <summary>
+    /// Gets or sets the device name.
+    /// </summary>
+    public string? DeviceName { get; set; }
+
+    /// <summary>
+    /// Gets or sets the remote endpoint.
+    /// </summary>
+    public string? RemoteEndPoint { get; set; }
+}

+ 38 - 0
MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs

@@ -0,0 +1,38 @@
+using System;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Controller.Events.Authentication;
+
+/// <summary>
+/// A class representing an authentication result event.
+/// </summary>
+public class AuthenticationResultEventArgs : EventArgs
+{
+    /// <summary>
+    /// Initializes a new instance of the <see cref="AuthenticationResultEventArgs"/> class.
+    /// </summary>
+    /// <param name="result">The <see cref="AuthenticationResult"/>.</param>
+    public AuthenticationResultEventArgs(AuthenticationResult result)
+    {
+        User = result.User;
+        SessionInfo = result.SessionInfo;
+        ServerId = result.ServerId;
+    }
+
+    /// <summary>
+    /// Gets or sets the user.
+    /// </summary>
+    public UserDto User { get; set; }
+
+    /// <summary>
+    /// Gets or sets the session information.
+    /// </summary>
+    public SessionInfo? SessionInfo { get; set; }
+
+    /// <summary>
+    /// Gets or sets the server id.
+    /// </summary>
+    public string? ServerId { get; set; }
+}

+ 1 - 1
MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs

@@ -96,7 +96,7 @@ namespace MediaBrowser.Controller.Net
         /// Starts sending messages over a web socket.
         /// </summary>
         /// <param name="message">The message.</param>
-        private void Start(WebSocketMessageInfo message)
+        protected virtual void Start(WebSocketMessageInfo message)
         {
             var vals = message.Data.Split(',');
 

+ 14 - 3
MediaBrowser.Controller/Net/IWebSocketConnection.cs

@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
 using System;
 using System.Net;
 using System.Net.WebSockets;
@@ -9,6 +7,9 @@ using MediaBrowser.Controller.Net.WebSocketMessages;
 
 namespace MediaBrowser.Controller.Net
 {
+    /// <summary>
+    /// Interface for WebSocket connections.
+    /// </summary>
     public interface IWebSocketConnection : IAsyncDisposable, IDisposable
     {
         /// <summary>
@@ -40,6 +41,11 @@ namespace MediaBrowser.Controller.Net
         /// <value>The state.</value>
         WebSocketState State { get; }
 
+        /// <summary>
+        /// Gets the authorization information.
+        /// </summary>
+        public AuthorizationInfo AuthorizationInfo { get; }
+
         /// <summary>
         /// Gets the remote end point.
         /// </summary>
@@ -65,6 +71,11 @@ namespace MediaBrowser.Controller.Net
         /// <exception cref="ArgumentNullException">The message is null.</exception>
         Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken);
 
-        Task ProcessAsync(CancellationToken cancellationToken = default);
+        /// <summary>
+        /// Receives a message asynchronously.
+        /// </summary>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task ReceiveAsync(CancellationToken cancellationToken = default);
     }
 }

+ 4 - 4
tests/Jellyfin.Server.Implementations.Tests/HttpServer/WebSocketConnectionTests.cs

@@ -13,7 +13,7 @@ namespace Jellyfin.Server.Implementations.Tests.HttpServer
         [Fact]
         public void DeserializeWebSocketMessage_SingleSegment_Success()
         {
-            var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!);
+            var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!, null!);
             var bytes = File.ReadAllBytes("Test Data/HttpServer/ForceKeepAlive.json");
             con.DeserializeWebSocketMessage(new ReadOnlySequence<byte>(bytes), out var bytesConsumed);
             Assert.Equal(109, bytesConsumed);
@@ -23,7 +23,7 @@ namespace Jellyfin.Server.Implementations.Tests.HttpServer
         public void DeserializeWebSocketMessage_MultipleSegments_Success()
         {
             const int SplitPos = 64;
-            var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!);
+            var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!, null!);
             var bytes = File.ReadAllBytes("Test Data/HttpServer/ForceKeepAlive.json");
             var seg1 = new BufferSegment(new Memory<byte>(bytes, 0, SplitPos));
             var seg2 = seg1.Append(new Memory<byte>(bytes, SplitPos, bytes.Length - SplitPos));
@@ -34,7 +34,7 @@ namespace Jellyfin.Server.Implementations.Tests.HttpServer
         [Fact]
         public void DeserializeWebSocketMessage_ValidPartial_Success()
         {
-            var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!);
+            var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!, null!);
             var bytes = File.ReadAllBytes("Test Data/HttpServer/ValidPartial.json");
             con.DeserializeWebSocketMessage(new ReadOnlySequence<byte>(bytes), out var bytesConsumed);
             Assert.Equal(109, bytesConsumed);
@@ -43,7 +43,7 @@ namespace Jellyfin.Server.Implementations.Tests.HttpServer
         [Fact]
         public void DeserializeWebSocketMessage_Partial_ThrowJsonException()
         {
-            var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!);
+            var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!, null!);
             var bytes = File.ReadAllBytes("Test Data/HttpServer/Partial.json");
             Assert.Throws<JsonException>(() => con.DeserializeWebSocketMessage(new ReadOnlySequence<byte>(bytes), out var bytesConsumed));
         }