diff options
13 files changed, 157 insertions, 75 deletions
diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs index 040b6b9e4..dd77b45d8 100644 --- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.HttpServer.Security var authorization = _authContext.GetAuthorizationInfo(requestContext); var user = authorization.User; - return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp(), user); + return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp().ToString(), user); } public SessionInfo GetSession(object requestContext) diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 43ee309b7..3c0d2aca1 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -21,6 +21,7 @@ using MediaBrowser.Model.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers { @@ -36,6 +37,7 @@ namespace Jellyfin.Api.Controllers private readonly IDeviceManager _deviceManager; private readonly IAuthorizationContext _authContext; private readonly IServerConfigurationManager _config; + private readonly ILogger _logger; /// <summary> /// Initializes a new instance of the <see cref="UserController"/> class. @@ -46,13 +48,15 @@ namespace Jellyfin.Api.Controllers /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> public UserController( IUserManager userManager, ISessionManager sessionManager, INetworkManager networkManager, IDeviceManager deviceManager, IAuthorizationContext authContext, - IServerConfigurationManager config) + IServerConfigurationManager config, + ILogger<UserController> logger) { _userManager = userManager; _sessionManager = sessionManager; @@ -60,6 +64,7 @@ namespace Jellyfin.Api.Controllers _deviceManager = deviceManager; _authContext = authContext; _config = config; + _logger = logger; } /// <summary> @@ -118,7 +123,7 @@ namespace Jellyfin.Api.Controllers return NotFound("User not found"); } - var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp()); + var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp().ToString()); return result; } @@ -204,7 +209,7 @@ namespace Jellyfin.Api.Controllers DeviceName = auth.Device, Password = request.Pw, PasswordSha1 = request.Password, - RemoteEndPoint = HttpContext.GetNormalizedRemoteIp(), + RemoteEndPoint = HttpContext.GetNormalizedRemoteIp().ToString(), Username = request.Username }).ConfigureAwait(false); @@ -291,7 +296,7 @@ namespace Jellyfin.Api.Controllers user.Username, request.CurrentPw, request.CurrentPw, - HttpContext.GetNormalizedRemoteIp(), + HttpContext.GetNormalizedRemoteIp().ToString(), false).ConfigureAwait(false); if (success == null) @@ -483,7 +488,7 @@ namespace Jellyfin.Api.Controllers await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false); } - var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp()); + var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp().ToString()); return result; } @@ -498,8 +503,14 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest) { + var ip = HttpContext.GetNormalizedRemoteIp(); var isLocal = HttpContext.IsLocal() - || _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp()); + || _networkManager.IsInLocalNetwork(ip); + + if (isLocal) + { + _logger.LogWarning("Password reset proccess initiated from outside the local network with IP: {IP}", ip); + } var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false); @@ -581,7 +592,7 @@ namespace Jellyfin.Api.Controllers var result = users .OrderBy(u => u.Username) - .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp())); + .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp().ToString())); return result; } diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 16380f0bb..751b48682 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -434,7 +434,7 @@ namespace Jellyfin.Api.Helpers } } - private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, string ipAddress) + private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, IPAddress ipAddress) { // Within the local network this will likely do more harm than good. if (_networkManager.IsInLocalNetwork(ipAddress)) diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index ce6740fc9..a2d0e7030 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.Linq; +using System.Net; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -179,7 +180,7 @@ namespace Jellyfin.Api.Helpers bool enableTranscoding, bool allowVideoStreamCopy, bool allowAudioStreamCopy, - string ipAddress) + IPAddress ipAddress) { var streamBuilder = new StreamBuilder(_mediaEncoder, _logger); @@ -551,7 +552,7 @@ namespace Jellyfin.Api.Helpers } } - private int? GetMaxBitrate(int? clientMaxBitrate, User user, string ipAddress) + private int? GetMaxBitrate(int? clientMaxBitrate, User user, IPAddress ipAddress) { var maxBitrate = clientMaxBitrate; var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0; diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 94856e03e..56585aeab 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -84,7 +84,7 @@ namespace Jellyfin.Api.Helpers authorization.Version, authorization.DeviceId, authorization.Device, - request.HttpContext.GetNormalizedRemoteIp(), + request.HttpContext.GetNormalizedRemoteIp().ToString(), user); if (session == null) diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 50d7612f2..a2c11cb8a 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -407,27 +407,18 @@ namespace Jellyfin.Server.Implementations.Users } var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); - bool success; - IAuthenticationProvider? authenticationProvider; + var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint) + .ConfigureAwait(false); + var authenticationProvider = authResult.authenticationProvider; + var success = authResult.success; - if (user != null) - { - var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint) - .ConfigureAwait(false); - authenticationProvider = authResult.authenticationProvider; - success = authResult.success; - } - else + if (user == null) { - var authResult = await AuthenticateLocalUser(username, password, null, remoteEndPoint) - .ConfigureAwait(false); - authenticationProvider = authResult.authenticationProvider; string updatedUsername = authResult.username; - success = authResult.success; if (success && authenticationProvider != null - && !(authenticationProvider is DefaultAuthenticationProvider)) + && authenticationProvider is not DefaultAuthenticationProvider) { // Trust the username returned by the authentication provider username = updatedUsername; diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 09799307b..e7f83aff1 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -37,11 +37,11 @@ <PackageReference Include="CommandLineParser" Version="2.8.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.3" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.4" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.3" /> <PackageReference Include="prometheus-net" Version="4.1.1" /> <PackageReference Include="prometheus-net.AspNetCore" Version="4.1.1" /> - <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" /> + <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" /> <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" /> <PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" /> diff --git a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs index 19fa95480..e51ad42d1 100644 --- a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs +++ b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs @@ -25,7 +25,7 @@ namespace MediaBrowser.Common.Extensions /// </summary> /// <param name="context">The HTTP context.</param> /// <returns>The remote caller IP address.</returns> - public static string GetNormalizedRemoteIp(this HttpContext context) + public static IPAddress GetNormalizedRemoteIp(this HttpContext context) { // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests) var ip = context.Connection.RemoteIpAddress ?? IPAddress.Loopback; @@ -35,7 +35,7 @@ namespace MediaBrowser.Common.Extensions ip = ip.MapToIPv4(); } - return ip.ToString(); + return ip; } } } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index bf33691c7..8299059a5 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -674,7 +674,7 @@ namespace MediaBrowser.Model.Dlna var videoStream = item.VideoStream; - // TODO: This doesn't accout for situation of device being able to handle media bitrate, but wifi connection not fast enough + // TODO: This doesn't account for situations where the device is able to handle the media's bitrate, but the connection isn't fast enough var directPlayEligibilityResult = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, subtitleStream, options, PlayMethod.DirectPlay); var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, subtitleStream, options, PlayMethod.DirectStream); bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult.Item1); @@ -1017,14 +1017,15 @@ namespace MediaBrowser.Model.Dlna } DeviceProfile profile = options.Profile; + string container = mediaSource.Container; // See if it can be direct played DirectPlayProfile directPlay = null; - foreach (var i in profile.DirectPlayProfiles) + foreach (var p in profile.DirectPlayProfiles) { - if (i.Type == DlnaProfileType.Video && IsVideoDirectPlaySupported(i, mediaSource, videoStream, audioStream)) + if (p.Type == DlnaProfileType.Video && IsVideoDirectPlaySupported(p, container, videoStream, audioStream)) { - directPlay = i; + directPlay = p; break; } } @@ -1032,23 +1033,23 @@ namespace MediaBrowser.Model.Dlna if (directPlay == null) { _logger.LogInformation( - "Profile: {0}, No video direct play profiles found for {1} with codec {2}", - profile?.Name ?? "Unknown Profile", - mediaSource?.Path ?? "Unknown path", - videoStream?.Codec ?? "Unknown codec"); + "Container: {Container}, Video: {Video}, Audio: {Audio} cannot be direct played by profile: {Profile} for path: {Path}", + container, + videoStream?.Codec ?? "no video", + audioStream?.Codec ?? "no audio", + profile.Name ?? "unknown profile", + mediaSource.Path ?? "unknown path"); return (null, GetTranscodeReasonsFromDirectPlayProfile(mediaSource, videoStream, audioStream, profile.DirectPlayProfiles)); } - string container = mediaSource.Container; - var conditions = new List<ProfileCondition>(); - foreach (var i in profile.ContainerProfiles) + foreach (var p in profile.ContainerProfiles) { - if (i.Type == DlnaProfileType.Video - && i.ContainsContainer(container)) + if (p.Type == DlnaProfileType.Video + && p.ContainsContainer(container)) { - foreach (var c in i.Conditions) + foreach (var c in p.Conditions) { conditions.Add(c); } @@ -1896,10 +1897,10 @@ namespace MediaBrowser.Model.Dlna return true; } - private bool IsVideoDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream) + private bool IsVideoDirectPlaySupported(DirectPlayProfile profile, string container, MediaStream videoStream, MediaStream audioStream) { // Check container type - if (!profile.SupportsContainer(item.Container)) + if (!profile.SupportsContainer(container)) { return false; } diff --git a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs index 09ffa8468..5b3d784ff 100644 --- a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Net; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; @@ -41,7 +42,7 @@ namespace Jellyfin.Api.Tests.Auth.LocalAccessPolicy public async Task LocalAccessOnly(bool isInLocalNetwork, bool shouldSucceed) { _networkManagerMock - .Setup(n => n.IsInLocalNetwork(It.IsAny<string>())) + .Setup(n => n.IsInLocalNetwork(It.IsAny<IPAddress>())) .Returns(isInLocalNetwork); TestHelpers.SetupConfigurationManager(_configurationManagerMock, true); diff --git a/tests/Jellyfin.Common.Tests/Extensions/CopyToExtensionsTests.cs b/tests/Jellyfin.Common.Tests/Extensions/CopyToExtensionsTests.cs new file mode 100644 index 000000000..9903409fa --- /dev/null +++ b/tests/Jellyfin.Common.Tests/Extensions/CopyToExtensionsTests.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Common.Extensions; +using Xunit; + +namespace Jellyfin.Common.Tests.Extensions +{ + public static class CopyToExtensionsTests + { + public static IEnumerable<object[]> CopyTo_Valid_Correct_TestData() + { + yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 0, new[] { 0, 1, 2, 3, 4, 5 } }; + yield return new object[] { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 2, new[] { 5, 4, 0, 1, 2, 0 } }; + } + + [Theory] + [MemberData(nameof(CopyTo_Valid_Correct_TestData))] + public static void CopyTo_Valid_Correct<T>(IReadOnlyList<T> source, IList<T> destination, int index, IList<T> expected) + { + source.CopyTo(destination, index); + Assert.Equal(expected, destination); + } + + public static IEnumerable<object[]> CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData() + { + yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, -1 }; + yield return new object[] { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 6 }; + yield return new object[] { new[] { 0, 1, 2 }, Array.Empty<int>(), 0 }; + yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0 }, 0 }; + yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 1 }; + } + + [Theory] + [MemberData(nameof(CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData))] + public static void CopyTo_Invalid_ThrowsArgumentOutOfRangeException<T>(IReadOnlyList<T> source, IList<T> destination, int index) + { + Assert.Throws<ArgumentOutOfRangeException>(() => source.CopyTo(destination, index)); + } + } +} diff --git a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs new file mode 100644 index 000000000..1cad625b7 --- /dev/null +++ b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs @@ -0,0 +1,63 @@ +using System.Net; +using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.Manager; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Jellyfin.Networking.Tests +{ + public class NetworkManagerTests + { + /// <summary> + /// Checks that the given IP address is in the specified network(s). + /// </summary> + /// <param name="network">Network address(es).</param> + /// <param name="value">The IP to check.</param> + [Theory] + [InlineData("192.168.2.1/24", "192.168.2.123")] + [InlineData("192.168.2.1/24, !192.168.2.122/32", "192.168.2.123")] + [InlineData("fd23:184f:2029:0::/56", "fd23:184f:2029:0:3139:7386:67d7:d517")] + [InlineData("fd23:184f:2029:0::/56, !fd23:184f:2029:0:3139:7386:67d7:d518/128", "fd23:184f:2029:0:3139:7386:67d7:d517")] + public void InNetwork_True_Success(string network, string value) + { + var ip = IPAddress.Parse(value); + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + LocalNetworkSubnets = network.Split(',') + }; + + using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger<NetworkManager>()); + + Assert.True(networkManager.IsInLocalNetwork(ip)); + } + + /// <summary> + /// Checks that thge given IP address is not in the network provided. + /// </summary> + /// <param name="network">Network address(es).</param> + /// <param name="value">The IP to check.</param> + [Theory] + [InlineData("192.168.10.0/24", "192.168.11.1")] + [InlineData("192.168.10.0/24, !192.168.10.60/32", "192.168.10.60")] + [InlineData("192.168.10.0/24", "fd23:184f:2029:0:3139:7386:67d7:d517")] + [InlineData("fd23:184f:2029:0::/56", "fd24:184f:2029:0:3139:7386:67d7:d517")] + [InlineData("fd23:184f:2029:0::/56, !fd23:184f:2029:0:3139:7386:67d7:d500/120", "fd23:184f:2029:0:3139:7386:67d7:d517")] + [InlineData("fd23:184f:2029:0::/56", "192.168.10.60")] + public void InNetwork_False_Success(string network, string value) + { + var ip = IPAddress.Parse(value); + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + LocalNetworkSubnets = network.Split(',') + }; + + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger<NetworkManager>()); + + Assert.False(nm.IsInLocalNetwork(ip)); + } + } +} diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 39fba1fb2..caea0d265 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -13,7 +13,7 @@ namespace Jellyfin.Networking.Tests { public class NetworkParseTests { - private static IConfigurationManager GetMockConfig(NetworkConfiguration conf) + internal static IConfigurationManager GetMockConfig(NetworkConfiguration conf) { var configManager = new Mock<IConfigurationManager> { @@ -55,32 +55,6 @@ namespace Jellyfin.Networking.Tests } /// <summary> - /// Check that the value given is in the network provided. - /// </summary> - /// <param name="network">Network address.</param> - /// <param name="value">Value to check.</param> - [Theory] - [InlineData("192.168.10.0/24, !192.168.10.60/32", "192.168.10.60")] - public void IsInNetwork(string network, string value) - { - if (network == null) - { - throw new ArgumentNullException(nameof(network)); - } - - var conf = new NetworkConfiguration() - { - EnableIPV6 = true, - EnableIPV4 = true, - LocalNetworkSubnets = network.Split(',') - }; - - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); - - Assert.False(nm.IsInLocalNetwork(value)); - } - - /// <summary> /// Checks IP address formats. /// </summary> /// <param name="address">IP Address.</param> |
