diff options
54 files changed, 346 insertions, 276 deletions
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index e227d5fe6..81693452f 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -181,7 +181,7 @@ jobs: inputs: sshEndpoint: repository runOptions: 'commands' - commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) & + commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) $(Build.SourceBranch) & - job: PublishNuget displayName: 'Publish NuGet packages' diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index 657850ac0..26a816107 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -395,6 +395,7 @@ namespace Emby.Dlna.ContentDirectory } writer.WriteFullEndElement(); + writer.Flush(); xmlWriter.WriteElementString("Result", builder.ToString()); } @@ -484,6 +485,7 @@ namespace Emby.Dlna.ContentDirectory } writer.WriteFullEndElement(); + writer.Flush(); xmlWriter.WriteElementString("Result", builder.ToString()); } diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 4c5dcdafc..2bf8eacb1 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -38,7 +38,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" /> </ItemGroup> <!-- Code Analyzers--> diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 73919f306..c17d355e5 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -3,6 +3,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -118,7 +119,7 @@ namespace Emby.Server.Implementations /// <summary> /// The disposable parts. /// </summary> - private readonly List<IDisposable> _disposableParts = new List<IDisposable>(); + private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new (); private readonly IFileSystem _fileSystemManager; private readonly IConfiguration _startupConfig; @@ -129,7 +130,6 @@ namespace Emby.Server.Implementations private List<Type> _creatingInstances; private IMediaEncoder _mediaEncoder; private ISessionManager _sessionManager; - private string[] _urlPrefixes; /// <summary> /// Gets or sets all concrete types. @@ -210,7 +210,7 @@ namespace Emby.Server.Implementations /// <summary> /// Gets the <see cref="INetworkManager"/> singleton instance. /// </summary> - public INetworkManager NetManager { get; internal set; } + public INetworkManager NetManager { get; private set; } /// <summary> /// Gets a value indicating whether this instance has changes that require the entire application to restart. @@ -232,16 +232,16 @@ namespace Emby.Server.Implementations protected ILoggerFactory LoggerFactory { get; } /// <summary> - /// Gets or sets the application paths. + /// Gets the application paths. /// </summary> /// <value>The application paths.</value> - protected IServerApplicationPaths ApplicationPaths { get; set; } + protected IServerApplicationPaths ApplicationPaths { get; } /// <summary> - /// Gets or sets the configuration manager. + /// Gets the configuration manager. /// </summary> /// <value>The configuration manager.</value> - public ServerConfigurationManager ConfigurationManager { get; set; } + public ServerConfigurationManager ConfigurationManager { get; } /// <summary> /// Gets or sets the service provider. @@ -345,22 +345,6 @@ namespace Emby.Server.Implementations } /// <summary> - /// Creates an instance of type and resolves all constructor dependencies. - /// </summary> - /// <param name="type">The type.</param> - /// <returns>System.Object.</returns> - public object CreateInstance(Type type) - => ActivatorUtilities.CreateInstance(ServiceProvider, type); - - /// <summary> - /// Creates an instance of type and resolves all constructor dependencies. - /// </summary> - /// <typeparam name="T">The type.</typeparam> - /// <returns>T.</returns> - public T CreateInstance<T>() - => ActivatorUtilities.CreateInstance<T>(ServiceProvider); - - /// <summary> /// Creates the instance safe. /// </summary> /// <param name="type">The type.</param> @@ -369,7 +353,7 @@ namespace Emby.Server.Implementations { _creatingInstances ??= new List<Type>(); - if (_creatingInstances.IndexOf(type) != -1) + if (_creatingInstances.Contains(type)) { Logger.LogError("DI Loop detected in the attempted creation of {Type}", type.FullName); foreach (var entry in _creatingInstances) @@ -379,7 +363,7 @@ namespace Emby.Server.Implementations _pluginManager.FailPlugin(type.Assembly); - throw new ExternalException("DI Loop detected."); + throw new TypeLoadException("DI Loop detected"); } try @@ -412,8 +396,15 @@ namespace Emby.Server.Implementations public IEnumerable<Type> GetExportTypes<T>() { var currentType = typeof(T); - - return _allConcreteTypes.Where(i => currentType.IsAssignableFrom(i)); + var numberOfConcreteTypes = _allConcreteTypes.Length; + for (var i = 0; i < numberOfConcreteTypes; i++) + { + var type = _allConcreteTypes[i]; + if (currentType.IsAssignableFrom(type)) + { + yield return type; + } + } } /// <inheritdoc /> @@ -428,9 +419,9 @@ namespace Emby.Server.Implementations if (manageLifetime) { - lock (_disposableParts) + foreach (var part in parts.OfType<IDisposable>()) { - _disposableParts.AddRange(parts.OfType<IDisposable>()); + _disposableParts.TryAdd(part, byte.MinValue); } } @@ -449,9 +440,9 @@ namespace Emby.Server.Implementations if (manageLifetime) { - lock (_disposableParts) + foreach (var part in parts.OfType<IDisposable>()) { - _disposableParts.AddRange(parts.OfType<IDisposable>()); + _disposableParts.TryAdd(part, byte.MinValue); } } @@ -563,7 +554,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager); serviceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager); serviceCollection.AddSingleton<IApplicationHost>(this); - serviceCollection.AddSingleton<IPluginManager>(_pluginManager); + serviceCollection.AddSingleton(_pluginManager); serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); serviceCollection.AddSingleton(_fileSystemManager); @@ -586,7 +577,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IZipClient, ZipClient>(); serviceCollection.AddSingleton<IServerApplicationHost>(this); - serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths); + serviceCollection.AddSingleton(ApplicationPaths); serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>(); @@ -790,8 +781,6 @@ namespace Emby.Server.Implementations _pluginManager.CreatePlugins(); - _urlPrefixes = GetUrlPrefixes().ToArray(); - Resolve<ILibraryManager>().AddParts( GetExports<IResolverIgnoreRule>(), GetExports<IItemResolver>(), @@ -859,32 +848,12 @@ namespace Emby.Server.Implementations } } - private IEnumerable<string> GetUrlPrefixes() - { - var hosts = new[] { "+" }; - - return hosts.SelectMany(i => - { - var prefixes = new List<string> - { - "http://" + i + ":" + HttpPort + "/" - }; - - if (Certificate != null) - { - prefixes.Add("https://" + i + ":" + HttpsPort + "/"); - } - - return prefixes; - }); - } - /// <summary> /// Called when [configuration updated]. /// </summary> /// <param name="sender">The sender.</param> /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param> - protected void OnConfigurationUpdated(object sender, EventArgs e) + private void OnConfigurationUpdated(object sender, EventArgs e) { var requiresRestart = false; var networkConfiguration = ConfigurationManager.GetNetworkConfiguration(); @@ -893,8 +862,8 @@ namespace Emby.Server.Implementations if (HttpPort != 0 && HttpsPort != 0) { // Need to restart if ports have changed - if (networkConfiguration.HttpServerPortNumber != HttpPort || - networkConfiguration.HttpsPortNumber != HttpsPort) + if (networkConfiguration.HttpServerPortNumber != HttpPort + || networkConfiguration.HttpsPortNumber != HttpsPort) { if (ConfigurationManager.Configuration.IsPortAuthorized) { @@ -906,11 +875,6 @@ namespace Emby.Server.Implementations } } - if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase)) - { - requiresRestart = true; - } - if (ValidateSslCertificate(networkConfiguration)) { requiresRestart = true; @@ -952,7 +916,7 @@ namespace Emby.Server.Implementations } /// <summary> - /// Notifies that the kernel that a change has been made that requires a restart. + /// Notifies the kernel that a change has been made that requires a restart. /// </summary> public void NotifyPendingRestart() { @@ -1093,11 +1057,6 @@ namespace Emby.Server.Implementations }; } - public IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo() - => NetManager.GetMacAddresses() - .Select(i => new WakeOnLanInfo(i)) - .ToList(); - public PublicSystemInfo GetPublicSystemInfo(HttpRequest request) { return new PublicSystemInfo @@ -1113,7 +1072,7 @@ namespace Emby.Server.Implementations } /// <inheritdoc/> - public string GetSmartApiUrl(IPAddress remoteAddr, int? port = null) + public string GetSmartApiUrl(IPAddress remoteAddr) { // Published server ends with a / if (!string.IsNullOrEmpty(PublishedServerUrl)) @@ -1122,12 +1081,12 @@ namespace Emby.Server.Implementations return PublishedServerUrl.Trim('/'); } - string smart = NetManager.GetBindInterface(remoteAddr, out port); + string smart = NetManager.GetBindInterface(remoteAddr, out var port); return GetLocalApiUrl(smart.Trim('/'), null, port); } /// <inheritdoc/> - public string GetSmartApiUrl(HttpRequest request, int? port = null) + public string GetSmartApiUrl(HttpRequest request) { // Return the host in the HTTP request as the API url if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest) @@ -1148,12 +1107,12 @@ namespace Emby.Server.Implementations return PublishedServerUrl.Trim('/'); } - string smart = NetManager.GetBindInterface(request, out port); + string smart = NetManager.GetBindInterface(request, out var port); return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port); } /// <inheritdoc/> - public string GetSmartApiUrl(string hostname, int? port = null) + public string GetSmartApiUrl(string hostname) { // Published server ends with a / if (!string.IsNullOrEmpty(PublishedServerUrl)) @@ -1162,7 +1121,7 @@ namespace Emby.Server.Implementations return PublishedServerUrl.Trim('/'); } - string smart = NetManager.GetBindInterface(hostname, out port); + string smart = NetManager.GetBindInterface(hostname, out var port); return GetLocalApiUrl(smart.Trim('/'), null, port); } @@ -1258,12 +1217,15 @@ namespace Emby.Server.Implementations Logger.LogInformation("Disposing {Type}", type.Name); - var parts = _disposableParts.Distinct().Where(i => i.GetType() != type).ToList(); - _disposableParts.Clear(); - - foreach (var part in parts) + foreach (var (part, _) in _disposableParts) { - Logger.LogInformation("Disposing {Type}", part.GetType().Name); + var partType = part.GetType(); + if (partType == type) + { + continue; + } + + Logger.LogInformation("Disposing {Type}", partType.Name); try { @@ -1271,9 +1233,11 @@ namespace Emby.Server.Implementations } catch (Exception ex) { - Logger.LogError(ex, "Error disposing {Type}", part.GetType().Name); + Logger.LogError(ex, "Error disposing {Type}", partType.Name); } } + + _disposableParts.Clear(); } _disposed = true; diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 673810c49..e9c005cea 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Security.Cryptography; +using System.Text; using MediaBrowser.Common.Extensions; using MediaBrowser.Model.Cryptography; -using static MediaBrowser.Common.Cryptography.Constants; +using static MediaBrowser.Model.Cryptography.Constants; namespace Emby.Server.Implementations.Cryptography { @@ -12,10 +14,7 @@ namespace Emby.Server.Implementations.Cryptography /// </summary> public class CryptographyProvider : ICryptoProvider { - // FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto - // Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1 - // there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one - // Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1 + // TODO: remove when not needed for backwards compat private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>() { "MD5", @@ -35,60 +34,81 @@ namespace Emby.Server.Implementations.Cryptography }; /// <inheritdoc /> - public string DefaultHashMethod => "PBKDF2"; + public string DefaultHashMethod => "PBKDF2-SHA512"; /// <inheritdoc /> - public IEnumerable<string> GetSupportedHashMethods() - => _supportedHashMethods; - - private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations) + public PasswordHash CreatePasswordHash(ReadOnlySpan<char> password) { - // downgrading for now as we need this library to be dotnetstandard compliant - // with this downgrade we'll add a check to make sure we're on the downgrade method at the moment - if (method != DefaultHashMethod) - { - throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}"); - } - - using var r = new Rfc2898DeriveBytes(bytes, salt, iterations); - return r.GetBytes(32); + byte[] salt = GenerateSalt(); + return new PasswordHash( + DefaultHashMethod, + Rfc2898DeriveBytes.Pbkdf2( + password, + salt, + DefaultIterations, + HashAlgorithmName.SHA512, + DefaultOutputLength), + salt, + new Dictionary<string, string> + { + { "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) } + }); } /// <inheritdoc /> - public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt) + public bool Verify(PasswordHash hash, ReadOnlySpan<char> password) { - if (hashMethod == DefaultHashMethod) + if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal)) { - return PBKDF2(hashMethod, bytes, salt, DefaultIterations); + return hash.Hash.SequenceEqual( + Rfc2898DeriveBytes.Pbkdf2( + password, + hash.Salt, + int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture), + HashAlgorithmName.SHA1, + 32)); } - if (!_supportedHashMethods.Contains(hashMethod)) + if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal)) { - throw new CryptographicException($"Requested hash method is not supported: {hashMethod}"); + return hash.Hash.SequenceEqual( + Rfc2898DeriveBytes.Pbkdf2( + password, + hash.Salt, + int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture), + HashAlgorithmName.SHA512, + DefaultOutputLength)); } - using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}."); - if (salt.Length == 0) + if (!_supportedHashMethods.Contains(hash.Id)) { - return h.ComputeHash(bytes); + throw new CryptographicException($"Requested hash method is not supported: {hash.Id}"); } - byte[] salted = new byte[bytes.Length + salt.Length]; + using var h = HashAlgorithm.Create(hash.Id) ?? throw new ResourceNotFoundException($"Unknown hash method: {hash.Id}."); + var bytes = Encoding.UTF8.GetBytes(password.ToArray()); + if (hash.Salt.Length == 0) + { + return hash.Hash.SequenceEqual(h.ComputeHash(bytes)); + } + + byte[] salted = new byte[bytes.Length + hash.Salt.Length]; Array.Copy(bytes, salted, bytes.Length); - Array.Copy(salt, 0, salted, bytes.Length, salt.Length); - return h.ComputeHash(salted); + hash.Salt.CopyTo(salted.AsSpan(bytes.Length)); + return hash.Hash.SequenceEqual(h.ComputeHash(salted)); } /// <inheritdoc /> - public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt) - => PBKDF2(DefaultHashMethod, bytes, salt, DefaultIterations); - - /// <inheritdoc /> public byte[] GenerateSalt() => GenerateSalt(DefaultSaltLength); /// <inheritdoc /> public byte[] GenerateSalt(int length) - => RandomNumberGenerator.GetBytes(length); + { + var salt = new byte[length]; + using var rng = RandomNumberGenerator.Create(); + rng.GetNonZeroBytes(salt); + return salt; + } } } diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs index 44dad5b17..11e33278d 100644 --- a/Emby.Server.Implementations/Data/ManagedConnection.cs +++ b/Emby.Server.Implementations/Data/ManagedConnection.cs @@ -7,7 +7,7 @@ using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data { - public class ManagedConnection : IDisposable + public sealed class ManagedConnection : IDisposable { private readonly SemaphoreSlim _writeLock; diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index e2ad07177..e7103ec95 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.HttpServer.Security if (!auth.HasToken) { - throw new AuthenticationException("Request does not contain a token."); + return auth; } if (!auth.IsAuthenticated) diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs index f86bfd755..e99876dce 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs @@ -35,7 +35,12 @@ namespace Emby.Server.Implementations.HttpServer /// <inheritdoc /> public async Task WebSocketRequestHandler(HttpContext context) { - _ = await _authService.Authenticate(context.Request).ConfigureAwait(false); + var authorizationInfo = await _authService.Authenticate(context.Request).ConfigureAwait(false); + if (!authorizationInfo.IsAuthenticated) + { + throw new SecurityException("Token is required"); + } + try { _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress); diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index 47a83d77c..e62361c1e 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -14,7 +12,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.IO { - public class FileRefresher : IDisposable + public sealed class FileRefresher : IDisposable { private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; @@ -22,7 +20,7 @@ namespace Emby.Server.Implementations.IO private readonly List<string> _affectedPaths = new List<string>(); private readonly object _timerLock = new object(); - private Timer _timer; + private Timer? _timer; private bool _disposed; public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger) @@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.IO AddPath(path); } - public event EventHandler<EventArgs> Completed; + public event EventHandler<EventArgs>? Completed; public string Path { get; private set; } @@ -111,7 +109,7 @@ namespace Emby.Server.Implementations.IO RestartTimer(); } - private void OnTimerCallback(object state) + private void OnTimerCallback(object? state) { List<string> paths; @@ -127,7 +125,7 @@ namespace Emby.Server.Implementations.IO try { - ProcessPathChanges(paths.ToList()); + ProcessPathChanges(paths); } catch (Exception ex) { @@ -137,12 +135,12 @@ namespace Emby.Server.Implementations.IO private void ProcessPathChanges(List<string> paths) { - var itemsToRefresh = paths + IEnumerable<BaseItem> itemsToRefresh = paths .Distinct(StringComparer.OrdinalIgnoreCase) .Select(GetAffectedBaseItem) .Where(item => item != null) - .GroupBy(x => x.Id) - .Select(x => x.First()); + .GroupBy(x => x!.Id) // Removed null values in the previous .Where() + .Select(x => x.First())!; foreach (var item in itemsToRefresh) { @@ -176,15 +174,15 @@ namespace Emby.Server.Implementations.IO /// </summary> /// <param name="path">The path.</param> /// <returns>BaseItem.</returns> - private BaseItem GetAffectedBaseItem(string path) + private BaseItem? GetAffectedBaseItem(string path) { - BaseItem item = null; + BaseItem? item = null; while (item == null && !string.IsNullOrEmpty(path)) { item = _libraryManager.FindByPath(path, null); - path = System.IO.Path.GetDirectoryName(path); + path = System.IO.Path.GetDirectoryName(path) ?? string.Empty; } if (item != null) diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index b525f5a2f..9fcc7fe59 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -449,12 +449,12 @@ namespace Emby.Server.Implementations.IO } var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger); - newRefresher.Completed += NewRefresher_Completed; + newRefresher.Completed += OnNewRefresherCompleted; _activeRefreshers.Add(newRefresher); } } - private void NewRefresher_Completed(object sender, EventArgs e) + private void OnNewRefresherCompleted(object sender, EventArgs e) { var refresher = (FileRefresher)sender; DisposeRefresher(refresher); @@ -481,6 +481,7 @@ namespace Emby.Server.Implementations.IO { lock (_activeRefreshers) { + refresher.Completed -= OnNewRefresherCompleted; refresher.Dispose(); _activeRefreshers.Remove(refresher); } @@ -492,6 +493,7 @@ namespace Emby.Server.Implementations.IO { foreach (var refresher in _activeRefreshers.ToList()) { + refresher.Completed -= OnNewRefresherCompleted; refresher.Dispose(); } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 5726d7158..a88a1fe84 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -187,7 +187,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV CultureInfo.InvariantCulture, "-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"", inputTempFile, - targetFile.Replace("\"", "\\\""), // Escape quotes in filename + targetFile.Replace("\"", "\\\"", StringComparison.Ordinal), // Escape quotes in filename videoArgs, GetAudioArgs(mediaSource), subtitleArgs, diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 1f963e4a2..615539db3 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -11,6 +11,7 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Mime; +using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Threading; @@ -648,7 +649,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings CancellationToken cancellationToken) { using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token"); - var hashedPasswordBytes = _cryptoProvider.ComputeHash("SHA1", Encoding.ASCII.GetBytes(password), Array.Empty<byte>()); + var hashedPasswordBytes = SHA1.HashData(Encoding.ASCII.GetBytes(password)); // TODO: remove ToLower when Convert.ToHexString supports lowercase // Schedules Direct requires the hex to be lowercase string hashedPassword = Convert.ToHexString(hashedPasswordBytes).ToLowerInvariant(); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index f9d151a81..9ab4cc628 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken) { using var client = new TcpClient(); - client.Connect(remoteIp, HdHomeRunPort); + await client.ConnectAsync(remoteIp, HdHomeRunPort).ConfigureAwait(false); using var stream = client.GetStream(); return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index 506ef5548..708ff52d7 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -283,7 +283,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts // #EXTINF:0,84.0 - VOX Schweiz if (!string.IsNullOrWhiteSpace(nameInExtInf)) { - var numberIndex = nameInExtInf.IndexOf(' '); + var numberIndex = nameInExtInf.IndexOf(' ', StringComparison.Ordinal); if (numberIndex > 0) { var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' }); diff --git a/Emby.Server.Implementations/Localization/Core/as.json b/Emby.Server.Implementations/Localization/Core/as.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/as.json @@ -0,0 +1 @@ +{} diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json new file mode 100644 index 000000000..56c4e7d39 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -0,0 +1,4 @@ +{ + "Sync": "Сінхранізацыя", + "Playlists": "Плэйліст" +} diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index db3c13d80..2dee5e327 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -25,7 +25,7 @@ "HeaderLiveTV": "TV en Directe", "HeaderNextUp": "A continuació", "HeaderRecordingGroups": "Grups d'Enregistrament", - "HomeVideos": "Vídeos domèstics", + "HomeVideos": "Vídeos Domèstics", "Inherit": "Hereta", "ItemAddedWithName": "{0} ha estat afegit a la biblioteca", "ItemRemovedWithName": "{0} ha estat eliminat de la biblioteca", @@ -39,7 +39,7 @@ "MixedContent": "Contingut barrejat", "Movies": "Pel·lícules", "Music": "Música", - "MusicVideos": "Vídeos musicals", + "MusicVideos": "Vídeos Musicals", "NameInstallFailed": "Instalació de {0} fallida", "NameSeasonNumber": "Temporada {0}", "NameSeasonUnknown": "Temporada Desconeguda", diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index e5405e515..626d76d6b 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -114,5 +114,7 @@ "Artists": "Esitajad", "Application": "Rakendus", "AppDeviceValues": "Rakendus: {0}, seade: {1}", - "Albums": "Albumid" + "Albums": "Albumid", + "UserOfflineFromDevice": "{0} katkestas ühenduse {1}-ga", + "SubtitleDownloadFailureFromForItem": "Subtiitrite allalaadimine {0} > {1} nurjus" } diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json index ba3513870..37d59abd9 100644 --- a/Emby.Server.Implementations/Localization/Core/id.json +++ b/Emby.Server.Implementations/Localization/Core/id.json @@ -7,10 +7,10 @@ "MessageApplicationUpdated": "Jellyfin Server sudah diperbarui", "Latest": "Terbaru", "LabelIpAddressValue": "Alamat IP: {0}", - "ItemRemovedWithName": "{0} sudah dikeluarkan dari pustaka", + "ItemRemovedWithName": "{0} sudah dihapus dari pustaka", "ItemAddedWithName": "{0} telah dimasukkan ke dalam pustaka", - "Inherit": "Warisan", - "HomeVideos": "Video Rumah", + "Inherit": "Warisi", + "HomeVideos": "Video Rumahan", "HeaderRecordingGroups": "Grup Rekaman", "HeaderNextUp": "Selanjutnya", "HeaderLiveTV": "TV Live", @@ -73,7 +73,7 @@ "NotificationOptionCameraImageUploaded": "Gambar kamera terunggah", "NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang", "NotificationOptionApplicationUpdateAvailable": "Pembaruan aplikasi tersedia", - "NewVersionIsAvailable": "Versi baru dari Jellyfin Server tersedia untuk diunduh.", + "NewVersionIsAvailable": "Versi baru dari Jellyfin Server sudah tersedia untuk diunduh.", "NameSeasonUnknown": "Musim tak diketahui", "NameSeasonNumber": "Musim {0}", "NameInstallFailed": "{0} penginstalan gagal", @@ -117,5 +117,7 @@ "TaskCleanActivityLog": "Bersihkan Log Aktivitas", "Undefined": "Tidak terdefinisi", "Forced": "Dipaksa", - "Default": "Bawaan" + "Default": "Bawaan", + "TaskOptimizeDatabaseDescription": "Rapihkan basis data dan membersihkan ruang kosong. Menjalankan tugas ini setelah memindai pustaka atau melakukan perubahan lain yang menyiratkan modifikasi basis data dapat meningkatkan kinerja.", + "TaskOptimizeDatabase": "Optimalkan basis data" } diff --git a/Emby.Server.Implementations/Localization/Core/te.json b/Emby.Server.Implementations/Localization/Core/te.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/te.json @@ -0,0 +1 @@ +{} diff --git a/Emby.Server.Implementations/Localization/Core/zu.json b/Emby.Server.Implementations/Localization/Core/zu.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/zu.json @@ -0,0 +1 @@ +{} diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index b07798fa4..9481e26f7 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -527,7 +527,7 @@ namespace Emby.Server.Implementations.Playlists var relativeUri = folderUri.MakeRelativeUri(fileAbsoluteUri); string relativePath = Uri.UnescapeDataString(relativeUri.ToString()); - if (fileAbsoluteUri.Scheme.Equals("file", StringComparison.CurrentCultureIgnoreCase)) + if (fileAbsoluteUri.Scheme.Equals("file", StringComparison.OrdinalIgnoreCase)) { relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); } diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs index bd1966623..67a9fbd3b 100644 --- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs +++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs @@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>System.Int32.</returns> public int Compare(BaseItem? x, BaseItem? y) { - return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase); + return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase); } /// <summary> diff --git a/Emby.Server.Implementations/Sorting/AlbumComparer.cs b/Emby.Server.Implementations/Sorting/AlbumComparer.cs index fe7dc84cb..4d09dda84 100644 --- a/Emby.Server.Implementations/Sorting/AlbumComparer.cs +++ b/Emby.Server.Implementations/Sorting/AlbumComparer.cs @@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>System.Int32.</returns> public int Compare(BaseItem? x, BaseItem? y) { - return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase); + return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase); } /// <summary> @@ -35,9 +35,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>System.String.</returns> private static string? GetValue(BaseItem? x) { - var audio = x as Audio; - - return audio == null ? string.Empty : audio.Album; + return x is Audio audio ? audio.Album : string.Empty; } } } diff --git a/Emby.Server.Implementations/Sorting/ArtistComparer.cs b/Emby.Server.Implementations/Sorting/ArtistComparer.cs index 7b7ba5753..a8bb55e2b 100644 --- a/Emby.Server.Implementations/Sorting/ArtistComparer.cs +++ b/Emby.Server.Implementations/Sorting/ArtistComparer.cs @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Sorting /// <inheritdoc /> public int Compare(BaseItem? x, BaseItem? y) { - return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase); + return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase); } /// <summary> diff --git a/Emby.Server.Implementations/Sorting/NameComparer.cs b/Emby.Server.Implementations/Sorting/NameComparer.cs index 8f87717f4..c2875eeb9 100644 --- a/Emby.Server.Implementations/Sorting/NameComparer.cs +++ b/Emby.Server.Implementations/Sorting/NameComparer.cs @@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.Sorting throw new ArgumentNullException(nameof(y)); } - return string.Compare(x.Name, y.Name, StringComparison.CurrentCultureIgnoreCase); + return string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase); } } } diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs index 4123a59f8..0bd9600b9 100644 --- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs @@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>System.Int32.</returns> public int Compare(BaseItem x, BaseItem y) { - return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase); + return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase); } private static string GetValue(BaseItem item) diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs index fb97a0349..79be9a89a 100644 --- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs @@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Sorting throw new ArgumentNullException(nameof(y)); } - return string.Compare(x.SortName, y.SortName, StringComparison.CurrentCultureIgnoreCase); + return string.Compare(x.SortName, y.SortName, StringComparison.OrdinalIgnoreCase); } } } diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index 369e846ae..bd3e7d9e3 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -45,6 +45,11 @@ namespace Jellyfin.Api.Auth try { var authorizationInfo = await _authService.Authenticate(Request).ConfigureAwait(false); + if (!authorizationInfo.HasToken) + { + return AuthenticateResult.NoResult(); + } + var role = UserRoles.User; if (authorizationInfo.IsApiKey || authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator)) { diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 904738bb4..2ff85fd2a 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -212,10 +212,13 @@ namespace Jellyfin.Api.Controllers /// <returns>An <see cref="IEnumerable{WakeOnLanInfo}"/> with the WakeOnLan infos.</returns> [HttpGet("WakeOnLanInfo")] [Authorize(Policy = Policies.DefaultAuthorization)] + [Obsolete("This endpoint is obsolete.")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo() { - var result = _appHost.GetWakeOnLanInfo(); + var result = _network.GetMacAddresses() + .Select(i => new WakeOnLanInfo(i)) + .ToList(); return Ok(result); } } diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index e1cbc6f33..3c079a71d 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -451,7 +451,7 @@ namespace Jellyfin.Api.Controllers if (@static.HasValue && @static.Value && state.DirectStreamProvider != null) { - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); + StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager); var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId); if (liveStreamInfo == null) @@ -467,7 +467,7 @@ namespace Jellyfin.Api.Controllers // Static remote stream if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http) { - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); + StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager); var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, HttpContext).ConfigureAwait(false); @@ -484,7 +484,7 @@ namespace Jellyfin.Api.Controllers var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); var isTranscodeCached = outputPathExists && transcodingJob != null; - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager); + StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, state.Request.StartTimeTicks, Request, _dlnaManager); // Static stream if (@static.HasValue && @static.Value) diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 58dd945c6..87233d907 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -24,7 +24,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" /> </ItemGroup> <!-- Code analysers--> diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index 3ab043c64..d59d36e88 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -185,9 +185,21 @@ namespace Jellyfin.Server.Implementations.Security authInfo.IsAuthenticated = true; authInfo.Client = key.Name; authInfo.Token = key.AccessToken; - authInfo.DeviceId = string.Empty; - authInfo.Device = string.Empty; - authInfo.Version = string.Empty; + if (string.IsNullOrWhiteSpace(authInfo.DeviceId)) + { + authInfo.DeviceId = string.Empty; + } + + if (string.IsNullOrWhiteSpace(authInfo.Device)) + { + authInfo.Device = string.Empty; + } + + if (string.IsNullOrWhiteSpace(authInfo.Version)) + { + authInfo.Version = string.Empty; + } + authInfo.IsApiKey = true; } } diff --git a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs index 6a78e7ee6..7480a05c2 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs @@ -1,9 +1,6 @@ using System; -using System.Linq; -using System.Text; using System.Threading.Tasks; using Jellyfin.Data.Entities; -using MediaBrowser.Common.Cryptography; using MediaBrowser.Controller.Authentication; using MediaBrowser.Model.Cryptography; @@ -61,35 +58,25 @@ namespace Jellyfin.Server.Implementations.Users } // Handle the case when the stored password is null, but the user tried to login with a password - if (resolvedUser.Password != null) + if (resolvedUser.Password == null) { - byte[] passwordBytes = Encoding.UTF8.GetBytes(password); - - PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password); - if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id) - || _cryptographyProvider.DefaultHashMethod == readyHash.Id) - { - byte[] calculatedHash = _cryptographyProvider.ComputeHash( - readyHash.Id, - passwordBytes, - readyHash.Salt.ToArray()); - - if (readyHash.Hash.SequenceEqual(calculatedHash)) - { - success = true; - } - } - else - { - throw new AuthenticationException($"Requested crypto method not available in provider: {readyHash.Id}"); - } + throw new AuthenticationException("Invalid username or password"); } + PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password); + success = _cryptographyProvider.Verify(readyHash, password); + if (!success) { throw new AuthenticationException("Invalid username or password"); } + // Migrate old hashes to the new default + if (!string.Equals(readyHash.Id, _cryptographyProvider.DefaultHashMethod, StringComparison.Ordinal)) + { + ChangePassword(resolvedUser, password); + } + return Task.FromResult(new ProviderAuthenticationResult { Username = username diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 8ca6e8d21..3d0a51ff6 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -5,7 +5,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Jellyfin.Data.Entities; @@ -13,7 +12,6 @@ using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Events.Users; using MediaBrowser.Common; -using MediaBrowser.Common.Cryptography; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; @@ -818,11 +816,7 @@ namespace Jellyfin.Server.Implementations.Users { // Check easy password var passwordHash = PasswordHash.Parse(user.EasyPassword); - var hash = _cryptoProvider.ComputeHash( - passwordHash.Id, - Encoding.UTF8.GetBytes(password), - passwordHash.Salt.ToArray()); - success = passwordHash.Hash.SequenceEqual(hash); + success = _cryptoProvider.Verify(passwordHash, password); } return (authenticationProvider, username, success); diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index e853609d6..fa98fda69 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -416,6 +416,18 @@ namespace Jellyfin.Server.Extensions } }) }); + + // Support dictionary with nullable string value. + options.MapType<Dictionary<string, string?>>(() => + new OpenApiSchema + { + Type = "object", + AdditionalProperties = new OpenApiSchema + { + Type = "string", + Nullable = true + } + }); } } } diff --git a/MediaBrowser.Common/Cryptography/CryptoExtensions.cs b/MediaBrowser.Common/Cryptography/CryptoExtensions.cs deleted file mode 100644 index 157b0ed10..000000000 --- a/MediaBrowser.Common/Cryptography/CryptoExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using System.Globalization; -using System.Text; -using MediaBrowser.Model.Cryptography; -using static MediaBrowser.Common.Cryptography.Constants; - -namespace MediaBrowser.Common.Cryptography -{ - /// <summary> - /// Class containing extension methods for working with Jellyfin cryptography objects. - /// </summary> - public static class CryptoExtensions - { - /// <summary> - /// Creates a new <see cref="PasswordHash" /> instance. - /// </summary> - /// <param name="cryptoProvider">The <see cref="ICryptoProvider" /> instance used.</param> - /// <param name="password">The password that will be hashed.</param> - /// <returns>A <see cref="PasswordHash" /> instance with the hash method, hash, salt and number of iterations.</returns> - public static PasswordHash CreatePasswordHash(this ICryptoProvider cryptoProvider, string password) - { - byte[] salt = cryptoProvider.GenerateSalt(); - return new PasswordHash( - cryptoProvider.DefaultHashMethod, - cryptoProvider.ComputeHashWithDefaultMethod( - Encoding.UTF8.GetBytes(password), - salt), - salt, - new Dictionary<string, string> - { - { "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) } - }); - } - } -} diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index e49ab41f4..53683cdbd 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -140,12 +140,5 @@ namespace MediaBrowser.Common /// </summary> /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param> void Init(IServiceCollection serviceCollection); - - /// <summary> - /// Creates the instance. - /// </summary> - /// <param name="type">The type.</param> - /// <returns>System.Object.</returns> - object CreateInstance(Type type); } } diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 441f06f69..4ed44baef 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -21,7 +21,7 @@ <ItemGroup> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" /> - <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs index abfdb41d8..ba2f419a2 100644 --- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs +++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs @@ -60,7 +60,7 @@ namespace MediaBrowser.Controller.BaseItemManager return false; } - var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); + var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, baseItem.GetType().Name, StringComparison.OrdinalIgnoreCase)); return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); } @@ -91,7 +91,7 @@ namespace MediaBrowser.Controller.BaseItemManager return false; } - var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); + var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, baseItem.GetType().Name, StringComparison.OrdinalIgnoreCase)); return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); } diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 7da492af3..8f8cf75a6 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -43,11 +43,6 @@ namespace MediaBrowser.Controller string FriendlyName { get; } /// <summary> - /// Gets the configured published server url. - /// </summary> - string PublishedServerUrl { get; } - - /// <summary> /// Gets the system info. /// </summary> /// <param name="request">The HTTP request.</param> @@ -60,25 +55,22 @@ namespace MediaBrowser.Controller /// Gets a URL specific for the request. /// </summary> /// <param name="request">The <see cref="HttpRequest"/> instance.</param> - /// <param name="port">Optional port number.</param> /// <returns>An accessible URL.</returns> - string GetSmartApiUrl(HttpRequest request, int? port = null); + string GetSmartApiUrl(HttpRequest request); /// <summary> /// Gets a URL specific for the request. /// </summary> /// <param name="remoteAddr">The remote <see cref="IPAddress"/> of the connection.</param> - /// <param name="port">Optional port number.</param> /// <returns>An accessible URL.</returns> - string GetSmartApiUrl(IPAddress remoteAddr, int? port = null); + string GetSmartApiUrl(IPAddress remoteAddr); /// <summary> /// Gets a URL specific for the request. /// </summary> /// <param name="hostname">The hostname used in the connection.</param> - /// <param name="port">Optional port number.</param> /// <returns>An accessible URL.</returns> - string GetSmartApiUrl(string hostname, int? port = null); + string GetSmartApiUrl(string hostname); /// <summary> /// Gets an URL that can be used to access the API over LAN. @@ -103,8 +95,6 @@ namespace MediaBrowser.Controller /// <returns>The API URL.</returns> string GetLocalApiUrl(string hostname, string scheme = null, int? port = null); - IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo(); - string ExpandVirtualPath(string path); string ReverseVirtualPath(string path); diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 1996335fe..cf3b7bc7a 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -14,10 +14,10 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Diacritics" Version="3.3.4" /> + <PackageReference Include="Diacritics" Version="3.3.10" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" /> - <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" /> <PackageReference Include="System.Threading.Tasks.Dataflow" Version="6.0.0" /> </ItemGroup> diff --git a/MediaBrowser.Common/Cryptography/Constants.cs b/MediaBrowser.Model/Cryptography/Constants.cs index 354114232..f2ebb5d3d 100644 --- a/MediaBrowser.Common/Cryptography/Constants.cs +++ b/MediaBrowser.Model/Cryptography/Constants.cs @@ -1,4 +1,4 @@ -namespace MediaBrowser.Common.Cryptography +namespace MediaBrowser.Model.Cryptography { /// <summary> /// Class containing global constants for Jellyfin Cryptography. @@ -8,11 +8,16 @@ namespace MediaBrowser.Common.Cryptography /// <summary> /// The default length for new salts. /// </summary> - public const int DefaultSaltLength = 64; + public const int DefaultSaltLength = 128 / 8; + + /// <summary> + /// The default output length. + /// </summary> + public const int DefaultOutputLength = 512 / 8; /// <summary> /// The default amount of iterations for hashing passwords. /// </summary> - public const int DefaultIterations = 1000; + public const int DefaultIterations = 120000; } } diff --git a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs index d8b7d848a..6c521578c 100644 --- a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs +++ b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs @@ -1,6 +1,6 @@ #pragma warning disable CS1591 -using System.Collections.Generic; +using System; namespace MediaBrowser.Model.Cryptography { @@ -8,11 +8,14 @@ namespace MediaBrowser.Model.Cryptography { string DefaultHashMethod { get; } - IEnumerable<string> GetSupportedHashMethods(); + /// <summary> + /// Creates a new <see cref="PasswordHash" /> instance. + /// </summary> + /// <param name="password">The password that will be hashed.</param> + /// <returns>A <see cref="PasswordHash" /> instance with the hash method, hash, salt and number of iterations.</returns> + PasswordHash CreatePasswordHash(ReadOnlySpan<char> password); - byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt); - - byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt); + bool Verify(PasswordHash hash, ReadOnlySpan<char> password); byte[] GenerateSalt(); diff --git a/MediaBrowser.Common/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs index 0e2065302..eec541041 100644 --- a/MediaBrowser.Common/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.Text; -namespace MediaBrowser.Common.Cryptography +namespace MediaBrowser.Model.Cryptography { // Defined from this hash storage spec // https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md diff --git a/MediaBrowser.Model/Dto/DisplayPreferencesDto.cs b/MediaBrowser.Model/Dto/DisplayPreferencesDto.cs index 6a4453536..90163ae91 100644 --- a/MediaBrowser.Model/Dto/DisplayPreferencesDto.cs +++ b/MediaBrowser.Model/Dto/DisplayPreferencesDto.cs @@ -63,10 +63,10 @@ namespace MediaBrowser.Model.Dto public int PrimaryImageWidth { get; set; } /// <summary> - /// Gets the custom prefs. + /// Gets or sets the custom prefs. /// </summary> /// <value>The custom prefs.</value> - public Dictionary<string, string?> CustomPrefs { get; } + public Dictionary<string, string?> CustomPrefs { get; set; } /// <summary> /// Gets or sets the scroll direction. diff --git a/MediaBrowser.Model/Entities/ImageType.cs b/MediaBrowser.Model/Entities/ImageType.cs index ee7410632..684b7390a 100644 --- a/MediaBrowser.Model/Entities/ImageType.cs +++ b/MediaBrowser.Model/Entities/ImageType.cs @@ -50,7 +50,10 @@ namespace MediaBrowser.Model.Entities /// <summary> /// The screenshot. /// </summary> - [Obsolete("Screenshot image type is no longer used.")] + /// <remarks> + /// This enum value is obsolete. + /// XmlSerializer does not serialize/deserialize objects that are marked as [Obsolete]. + /// </remarks> Screenshot = 8, /// <summary> diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 85947b3de..70fef5d66 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -29,7 +29,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" /> <PackageReference Include="System.Globalization" Version="4.3.0" /> <PackageReference Include="System.Text.Json" Version="6.0.0" /> diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 5e1985611..b1d73c4c4 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -457,7 +457,7 @@ namespace MediaBrowser.Providers.Manager CancellationToken cancellationToken) { var eligibleImages = images - .Where(i => i.Type == type && i.Width >= minWidth) + .Where(i => i.Type == type && (i.Width == null || i.Width >= minWidth)) .ToList(); if (EnableImageStub(item) && eligibleImages.Count > 0) diff --git a/RSSDP/HttpParserBase.cs b/RSSDP/HttpParserBase.cs index c56249523..6b6c13d99 100644 --- a/RSSDP/HttpParserBase.cs +++ b/RSSDP/HttpParserBase.cs @@ -82,7 +82,7 @@ namespace Rssdp.Infrastructure throw new ArgumentNullException(nameof(versionData)); } - var versionSeparatorIndex = versionData.IndexOf('/'); + var versionSeparatorIndex = versionData.IndexOf('/', StringComparison.Ordinal); if (versionSeparatorIndex <= 0 || versionSeparatorIndex == versionData.Length) { throw new ArgumentException("request header line is invalid. Http Version not supplied or incorrect format.", nameof(versionData)); @@ -101,7 +101,7 @@ namespace Rssdp.Infrastructure { // Header format is // name: value - var headerKeySeparatorIndex = line.IndexOf(":", StringComparison.OrdinalIgnoreCase); + var headerKeySeparatorIndex = line.IndexOf(':', StringComparison.Ordinal); var headerName = line.Substring(0, headerKeySeparatorIndex).Trim(); var headerValue = line.Substring(headerKeySeparatorIndex + 1).Trim(); @@ -172,7 +172,7 @@ namespace Rssdp.Infrastructure else { var segments = headerValue.Split(SeparatorCharacters); - if (headerValue.Contains('"')) + if (headerValue.Contains('"', StringComparison.Ordinal)) { for (int segmentIndex = 0; segmentIndex < segments.Length; segmentIndex++) { diff --git a/jellyfin.ruleset b/jellyfin.ruleset index e14c1c427..469f61021 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -38,8 +38,14 @@ </Rules> <Rules AnalyzerId="Microsoft.CodeAnalysis.NetAnalyzers" RuleNamespace="Microsoft.Design"> + <!-- error on CA1063: Implement IDisposable correctly --> + <Rule Id="CA1063" Action="Error" /> <!-- error on CA1305: Specify IFormatProvider --> <Rule Id="CA1305" Action="Error" /> + <!-- error on CA1307: Specify StringComparison for clarity --> + <Rule Id="CA1307" Action="Error" /> + <!-- error on CA1309: Use ordinal StringComparison --> + <Rule Id="CA1309" Action="Error" /> <!-- error on CA1725: Parameter names should match base declaration --> <Rule Id="CA1725" Action="Error" /> <!-- error on CA1725: Call async methods when in an async method --> diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs index cd03958b6..6f5c0ed0c 100644 --- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs @@ -132,6 +132,8 @@ namespace Jellyfin.Api.Tests.Auth authorizationInfo.User.AddDefaultPreferences(); authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin); authorizationInfo.IsApiKey = false; + authorizationInfo.HasToken = true; + authorizationInfo.Token = "fake-token"; _jellyfinAuthServiceMock.Setup( a => a.Authenticate( diff --git a/tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs b/tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs new file mode 100644 index 000000000..edceef4a7 --- /dev/null +++ b/tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs @@ -0,0 +1,89 @@ +using System; +using MediaBrowser.Controller.BaseItemManager; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Model.Configuration; +using Moq; +using Xunit; + +namespace Jellyfin.Controller.Tests +{ + public class BaseItemManagerTests + { + [Theory] + [InlineData(typeof(Book), "LibraryEnabled", true)] + [InlineData(typeof(Book), "LibraryDisabled", false)] + [InlineData(typeof(MusicArtist), "Enabled", true)] + [InlineData(typeof(MusicArtist), "ServerDisabled", false)] + public void IsMetadataFetcherEnabled_ChecksOptions_ReturnsExpected(Type itemType, string fetcherName, bool expected) + { + BaseItem item = (BaseItem)Activator.CreateInstance(itemType)!; + + var libraryOptions = new LibraryOptions + { + TypeOptions = new[] + { + new TypeOptions + { + Type = "Book", + MetadataFetchers = new[] { "LibraryEnabled" } + } + } + }; + + var serverConfiguration = new ServerConfiguration(); + foreach (var typeConfig in serverConfiguration.MetadataOptions) + { + typeConfig.DisabledMetadataFetchers = new[] { "ServerDisabled" }; + } + + var serverConfigurationManager = new Mock<IServerConfigurationManager>(); + serverConfigurationManager.Setup(scm => scm.Configuration) + .Returns(serverConfiguration); + + var baseItemManager = new BaseItemManager(serverConfigurationManager.Object); + var actual = baseItemManager.IsMetadataFetcherEnabled(item, libraryOptions, fetcherName); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(typeof(Book), "LibraryEnabled", true)] + [InlineData(typeof(Book), "LibraryDisabled", false)] + [InlineData(typeof(MusicArtist), "Enabled", true)] + [InlineData(typeof(MusicArtist), "ServerDisabled", false)] + public void IsImageFetcherEnabled_ChecksOptions_ReturnsExpected(Type itemType, string fetcherName, bool expected) + { + BaseItem item = (BaseItem)Activator.CreateInstance(itemType)!; + + var libraryOptions = new LibraryOptions + { + TypeOptions = new[] + { + new TypeOptions + { + Type = "Book", + ImageFetchers = new[] { "LibraryEnabled" } + } + } + }; + + var serverConfiguration = new ServerConfiguration(); + foreach (var typeConfig in serverConfiguration.MetadataOptions) + { + typeConfig.DisabledImageFetchers = new[] { "ServerDisabled" }; + } + + var serverConfigurationManager = new Mock<IServerConfigurationManager>(); + serverConfigurationManager.Setup(scm => scm.Configuration) + .Returns(serverConfiguration); + + var baseItemManager = new BaseItemManager(serverConfigurationManager.Object); + var actual = baseItemManager.IsImageFetcherEnabled(item, libraryOptions, fetcherName); + + Assert.Equal(expected, actual); + } + } +} diff --git a/tests/Jellyfin.Common.Tests/Cryptography/PasswordHashTests.cs b/tests/Jellyfin.Model.Tests/Cryptography/PasswordHashTests.cs index bfece97b6..6948280a3 100644 --- a/tests/Jellyfin.Common.Tests/Cryptography/PasswordHashTests.cs +++ b/tests/Jellyfin.Model.Tests/Cryptography/PasswordHashTests.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using MediaBrowser.Common.Cryptography; +using MediaBrowser.Model.Cryptography; using Xunit; -namespace Jellyfin.Common.Tests.Cryptography +namespace Jellyfin.Model.Tests.Cryptography { public static class PasswordHashTests { |
