From 4519ce26e2250cb233836296d292ddb7b3cf6346 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Thu, 31 Jan 2019 00:24:53 -0800 Subject: Upgrade crypto provider, retarget better framework --- Emby.Server.Implementations/Library/UserManager.cs | 31 +++++++++++----------- 1 file changed, 15 insertions(+), 16 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 05fce4542..70639dad5 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Events; @@ -220,22 +221,20 @@ namespace Emby.Server.Implementations.Library } } - public bool IsValidUsername(string username) - { - // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) - foreach (var currentChar in username) - { - if (!IsValidUsernameCharacter(currentChar)) - { - return false; - } - } - return true; - } - - private static bool IsValidUsernameCharacter(char i) - { - return !char.Equals(i, '<') && !char.Equals(i, '>'); + public bool IsValidUsername(string username) + { + //The old way was dumb, we should make it less dumb, lets do so. + //This is some regex that matches only on unicode "word" characters, as well as -, _ and @ + //In theory this will cut out most if not all 'control' characters which should help minimize any weirdness + string UserNameRegex = "^[\\w-'._@]*$"; + // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) + return Regex.IsMatch(username, UserNameRegex); + } + + private static bool IsValidUsernameCharacter(char i) + { + string UserNameRegex = "^[\\w-'._@]*$"; + return Regex.IsMatch(i.ToString(), UserNameRegex); } public string MakeValidUsername(string username) -- cgit v1.2.3 From 05bbf71b6d97614888efe103f763753e4487cc2c Mon Sep 17 00:00:00 2001 From: Phallacy Date: Tue, 12 Feb 2019 02:16:03 -0800 Subject: sha256 with salt auth and sha1 interop --- .../Cryptography/CryptographyProvider.cs | 2 +- .../Library/DefaultAuthenticationProvider.cs | 167 +- Emby.Server.Implementations/Library/UserManager.cs | 2416 ++++++++++---------- MediaBrowser.Model/Cryptography/ICryptoProvider.cs | 29 +- MediaBrowser.Model/Cryptography/PasswordHash.cs | 37 +- 5 files changed, 1388 insertions(+), 1263 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index ca6ae2bb2..4f2bc1b03 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -10,7 +10,7 @@ namespace Emby.Server.Implementations.Cryptography public class CryptographyProvider : ICryptoProvider { private List SupportedHashMethods = new List(); - private string DefaultHashMethod = "SHA256"; + public string DefaultHashMethod => "SHA256"; private RandomNumberGenerator rng; private int defaultiterations = 1000; public CryptographyProvider() diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 4013ac0c8..92346c65a 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Text; using System.Threading.Tasks; using MediaBrowser.Controller.Authentication; @@ -19,31 +20,110 @@ namespace Emby.Server.Implementations.Library public bool IsEnabled => true; + + //This is dumb and an artifact of the backwards way auth providers were designed. + //This version of authenticate was never meant to be called, but needs to be here for interface compat + //Only the providers that don't provide local user support use this public Task Authenticate(string username, string password) { throw new NotImplementedException(); } - public Task Authenticate(string username, string password, User resolvedUser) - { - if (resolvedUser == null) - { - throw new Exception("Invalid username or password"); - } - - var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); - if (!success) - { - throw new Exception("Invalid username or password"); - } + //This is the verson that we need to use for local users. Because reasons. + public Task Authenticate(string username, string password, User resolvedUser) + { + ConvertPasswordFormat(resolvedUser); + byte[] passwordbytes = Encoding.UTF8.GetBytes(password); + bool success = false; + if (resolvedUser == null) + { + success = false; + throw new Exception("Invalid username or password"); + } + if (!resolvedUser.Password.Contains("$")) + { + ConvertPasswordFormat(resolvedUser); + } + PasswordHash ReadyHash = new PasswordHash(resolvedUser.Password); + byte[] CalculatedHash; + string CalculatedHashString; + if (_cryptographyProvider.GetSupportedHashMethods().Any(i => i == ReadyHash.Id)) + { + if (String.IsNullOrEmpty(ReadyHash.Salt)) + { + CalculatedHash = _cryptographyProvider.ComputeHash(ReadyHash.Id, passwordbytes); + CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); + } + else + { + CalculatedHash = _cryptographyProvider.ComputeHash(ReadyHash.Id, passwordbytes, ReadyHash.SaltBytes); + CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); + } + if (CalculatedHashString == ReadyHash.Hash) + { + success = true; + //throw new Exception("Invalid username or password"); + } + } + else + { + success = false; + throw new Exception(String.Format("Requested crypto method not available in provider: {0}", ReadyHash.Id)); + } + + //var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); + + if (!success) + { + throw new Exception("Invalid username or password"); + } + + return Task.FromResult(new ProviderAuthenticationResult + { + Username = username + }); + } - return Task.FromResult(new ProviderAuthenticationResult - { - Username = username - }); + //This allows us to move passwords forward to the newformat without breaking. They are still insecure, unsalted, and dumb before a password change + //but at least they are in the new format. + private void ConvertPasswordFormat(User user) + { + if (!string.IsNullOrEmpty(user.Password)) + { + if (!user.Password.Contains("$")) + { + string hash = user.Password; + user.Password = String.Format("$SHA1${0}", hash); + } + if (user.EasyPassword != null && !user.EasyPassword.Contains("$")) + { + string hash = user.EasyPassword; + user.EasyPassword = String.Format("$SHA1${0}", hash); + } + } } + // OLD VERSION //public Task Authenticate(string username, string password, User resolvedUser) + // OLD VERSION //{ + // OLD VERSION // if (resolvedUser == null) + // OLD VERSION // { + // OLD VERSION // throw new Exception("Invalid username or password"); + // OLD VERSION // } + // OLD VERSION // + // OLD VERSION // var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); + // OLD VERSION // + // OLD VERSION // if (!success) + // OLD VERSION // { + // OLD VERSION // throw new Exception("Invalid username or password"); + // OLD VERSION // } + // OLD VERSION // + // OLD VERSION // return Task.FromResult(new ProviderAuthenticationResult + // OLD VERSION // { + // OLD VERSION // Username = username + // OLD VERSION // }); + // OLD VERSION //} + public Task HasPassword(User user) { var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user)); @@ -57,19 +137,26 @@ namespace Emby.Server.Implementations.Library public Task ChangePassword(User user, string newPassword) { - string newPasswordHash = null; - - if (newPassword != null) + //string newPasswordHash = null; + ConvertPasswordFormat(user); + PasswordHash passwordHash = new PasswordHash(user.Password); + if(passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt)) { - newPasswordHash = GetHashedString(user, newPassword); + passwordHash.SaltBytes = _cryptographyProvider.GenerateSalt(); + passwordHash.Salt = BitConverter.ToString(passwordHash.SaltBytes).Replace("-",""); + passwordHash.Id = _cryptographyProvider.DefaultHashMethod; + passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash); + }else if (newPassword != null) + { + passwordHash.Hash = GetHashedString(user, newPassword); } - if (string.IsNullOrWhiteSpace(newPasswordHash)) + if (string.IsNullOrWhiteSpace(passwordHash.Hash)) { - throw new ArgumentNullException(nameof(newPasswordHash)); + throw new ArgumentNullException(nameof(passwordHash.Hash)); } - user.Password = newPasswordHash; + user.Password = passwordHash.ToString(); return Task.CompletedTask; } @@ -86,19 +173,39 @@ namespace Emby.Server.Implementations.Library return GetHashedString(user, string.Empty); } + public string GetHashedStringChangeAuth(string NewPassword, PasswordHash passwordHash) + { + return BitConverter.ToString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(NewPassword), passwordHash.SaltBytes)).Replace("-", string.Empty); + } + /// /// Gets the hashed string. /// - public string GetHashedString(User user, string str) - { - var salt = user.Salt; - if (salt != null) + public string GetHashedString(User user, string str) + { + //This is legacy. Deprecated in the auth method. + //return BitConverter.ToString(_cryptoProvider2.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty); + PasswordHash passwordHash; + if (String.IsNullOrEmpty(user.Password)) + { + passwordHash = new PasswordHash(_cryptographyProvider); + } + else { - // return BCrypt.HashPassword(str, salt); + ConvertPasswordFormat(user); + passwordHash = new PasswordHash(user.Password); + } + if (passwordHash.SaltBytes != null) + { + return BitConverter.ToString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str), passwordHash.SaltBytes)).Replace("-",string.Empty); + } + else + { + return BitConverter.ToString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty); + //throw new Exception("User does not have a hash, this should not be possible"); } - // legacy - return BitConverter.ToString(_cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty); + } } } diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 40eda52c6..a139c4e73 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -1,222 +1,222 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Events; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Security; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Library -{ - /// - /// Class UserManager - /// - public class UserManager : IUserManager - { - /// - /// Gets the users. - /// - /// The users. - public IEnumerable Users => _users; - - private User[] _users; - - /// - /// The _logger - /// - private readonly ILogger _logger; - - /// - /// Gets or sets the configuration manager. - /// - /// The configuration manager. - private IServerConfigurationManager ConfigurationManager { get; set; } - - /// - /// Gets the active user repository - /// - /// The user repository. - private IUserRepository UserRepository { get; set; } - public event EventHandler> UserPasswordChanged; - - private readonly IXmlSerializer _xmlSerializer; - private readonly IJsonSerializer _jsonSerializer; - - private readonly INetworkManager _networkManager; - - private readonly Func _imageProcessorFactory; - private readonly Func _dtoServiceFactory; - private readonly IServerApplicationHost _appHost; - private readonly IFileSystem _fileSystem; - private readonly ICryptoProvider _cryptographyProvider; - - private IAuthenticationProvider[] _authenticationProviders; - private DefaultAuthenticationProvider _defaultAuthenticationProvider; - - public UserManager( - ILoggerFactory loggerFactory, - IServerConfigurationManager configurationManager, - IUserRepository userRepository, - IXmlSerializer xmlSerializer, - INetworkManager networkManager, - Func imageProcessorFactory, - Func dtoServiceFactory, - IServerApplicationHost appHost, - IJsonSerializer jsonSerializer, - IFileSystem fileSystem, - ICryptoProvider cryptographyProvider) - { - _logger = loggerFactory.CreateLogger(nameof(UserManager)); - UserRepository = userRepository; - _xmlSerializer = xmlSerializer; - _networkManager = networkManager; - _imageProcessorFactory = imageProcessorFactory; - _dtoServiceFactory = dtoServiceFactory; - _appHost = appHost; - _jsonSerializer = jsonSerializer; - _fileSystem = fileSystem; - _cryptographyProvider = cryptographyProvider; - ConfigurationManager = configurationManager; - _users = Array.Empty(); - - DeletePinFile(); - } - - public NameIdPair[] GetAuthenticationProviders() - { - return _authenticationProviders - .Where(i => i.IsEnabled) - .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1) - .ThenBy(i => i.Name) - .Select(i => new NameIdPair - { - Name = i.Name, - Id = GetAuthenticationProviderId(i) - }) - .ToArray(); - } - - public void AddParts(IEnumerable authenticationProviders) - { - _authenticationProviders = authenticationProviders.ToArray(); - - _defaultAuthenticationProvider = _authenticationProviders.OfType().First(); - } - - #region UserUpdated Event - /// - /// Occurs when [user updated]. - /// - public event EventHandler> UserUpdated; - public event EventHandler> UserPolicyUpdated; - public event EventHandler> UserConfigurationUpdated; - public event EventHandler> UserLockedOut; - - /// - /// Called when [user updated]. - /// - /// The user. - private void OnUserUpdated(User user) - { - UserUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); - } - #endregion - - #region UserDeleted Event - /// - /// Occurs when [user deleted]. - /// - public event EventHandler> UserDeleted; - /// - /// Called when [user deleted]. - /// - /// The user. - private void OnUserDeleted(User user) - { - UserDeleted?.Invoke(this, new GenericEventArgs { Argument = user }); - } - #endregion - - /// - /// Gets a User by Id - /// - /// The id. - /// User. - /// - public User GetUserById(Guid id) - { - if (id.Equals(Guid.Empty)) - { - throw new ArgumentNullException(nameof(id)); - } - - return Users.FirstOrDefault(u => u.Id == id); - } - - /// - /// Gets the user by identifier. - /// - /// The identifier. - /// User. - public User GetUserById(string id) - { - return GetUserById(new Guid(id)); - } - - public User GetUserByName(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase)); - } - - public void Initialize() - { - _users = LoadUsers(); - - var users = Users.ToList(); - - // If there are no local users with admin rights, make them all admins - if (!users.Any(i => i.Policy.IsAdministrator)) - { - foreach (var user in users) - { - user.Policy.IsAdministrator = true; - UpdateUserPolicy(user, user.Policy, false); - } - } - } - +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Events; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Users; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.Library +{ + /// + /// Class UserManager + /// + public class UserManager : IUserManager + { + /// + /// Gets the users. + /// + /// The users. + public IEnumerable Users => _users; + + private User[] _users; + + /// + /// The _logger + /// + private readonly ILogger _logger; + + /// + /// Gets or sets the configuration manager. + /// + /// The configuration manager. + private IServerConfigurationManager ConfigurationManager { get; set; } + + /// + /// Gets the active user repository + /// + /// The user repository. + private IUserRepository UserRepository { get; set; } + public event EventHandler> UserPasswordChanged; + + private readonly IXmlSerializer _xmlSerializer; + private readonly IJsonSerializer _jsonSerializer; + + private readonly INetworkManager _networkManager; + + private readonly Func _imageProcessorFactory; + private readonly Func _dtoServiceFactory; + private readonly IServerApplicationHost _appHost; + private readonly IFileSystem _fileSystem; + private readonly ICryptoProvider _cryptographyProvider; + + private IAuthenticationProvider[] _authenticationProviders; + private DefaultAuthenticationProvider _defaultAuthenticationProvider; + + public UserManager( + ILoggerFactory loggerFactory, + IServerConfigurationManager configurationManager, + IUserRepository userRepository, + IXmlSerializer xmlSerializer, + INetworkManager networkManager, + Func imageProcessorFactory, + Func dtoServiceFactory, + IServerApplicationHost appHost, + IJsonSerializer jsonSerializer, + IFileSystem fileSystem, + ICryptoProvider cryptographyProvider) + { + _logger = loggerFactory.CreateLogger(nameof(UserManager)); + UserRepository = userRepository; + _xmlSerializer = xmlSerializer; + _networkManager = networkManager; + _imageProcessorFactory = imageProcessorFactory; + _dtoServiceFactory = dtoServiceFactory; + _appHost = appHost; + _jsonSerializer = jsonSerializer; + _fileSystem = fileSystem; + _cryptographyProvider = cryptographyProvider; + ConfigurationManager = configurationManager; + _users = Array.Empty(); + + DeletePinFile(); + } + + public NameIdPair[] GetAuthenticationProviders() + { + return _authenticationProviders + .Where(i => i.IsEnabled) + .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1) + .ThenBy(i => i.Name) + .Select(i => new NameIdPair + { + Name = i.Name, + Id = GetAuthenticationProviderId(i) + }) + .ToArray(); + } + + public void AddParts(IEnumerable authenticationProviders) + { + _authenticationProviders = authenticationProviders.ToArray(); + + _defaultAuthenticationProvider = _authenticationProviders.OfType().First(); + } + + #region UserUpdated Event + /// + /// Occurs when [user updated]. + /// + public event EventHandler> UserUpdated; + public event EventHandler> UserPolicyUpdated; + public event EventHandler> UserConfigurationUpdated; + public event EventHandler> UserLockedOut; + + /// + /// Called when [user updated]. + /// + /// The user. + private void OnUserUpdated(User user) + { + UserUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); + } + #endregion + + #region UserDeleted Event + /// + /// Occurs when [user deleted]. + /// + public event EventHandler> UserDeleted; + /// + /// Called when [user deleted]. + /// + /// The user. + private void OnUserDeleted(User user) + { + UserDeleted?.Invoke(this, new GenericEventArgs { Argument = user }); + } + #endregion + + /// + /// Gets a User by Id + /// + /// The id. + /// User. + /// + public User GetUserById(Guid id) + { + if (id.Equals(Guid.Empty)) + { + throw new ArgumentNullException(nameof(id)); + } + + return Users.FirstOrDefault(u => u.Id == id); + } + + /// + /// Gets the user by identifier. + /// + /// The identifier. + /// User. + public User GetUserById(string id) + { + return GetUserById(new Guid(id)); + } + + public User GetUserByName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase)); + } + + public void Initialize() + { + _users = LoadUsers(); + + var users = Users.ToList(); + + // If there are no local users with admin rights, make them all admins + if (!users.Any(i => i.Policy.IsAdministrator)) + { + foreach (var user in users) + { + user.Policy.IsAdministrator = true; + UpdateUserPolicy(user, user.Policy, false); + } + } + } + public bool IsValidUsername(string username) { //The old way was dumb, we should make it less dumb, lets do so. @@ -231,992 +231,992 @@ namespace Emby.Server.Implementations.Library { string UserNameRegex = "^[\\w-'._@]*$"; return Regex.IsMatch(i.ToString(), UserNameRegex); - } - - public string MakeValidUsername(string username) - { - if (IsValidUsername(username)) - { - return username; - } - - // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) - var builder = new StringBuilder(); - - foreach (var c in username) - { - if (IsValidUsernameCharacter(c)) - { - builder.Append(c); - } - } - return builder.ToString(); - } - - public async Task AuthenticateUser(string username, string password, string hashedPassword, string remoteEndPoint, bool isUserSession) - { - if (string.IsNullOrWhiteSpace(username)) - { - throw new ArgumentNullException(nameof(username)); - } - - var user = Users - .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); - - var success = false; - IAuthenticationProvider authenticationProvider = null; - - if (user != null) - { - var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false); - authenticationProvider = authResult.Item1; - success = authResult.Item2; - } - else - { - // user is null - var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); - authenticationProvider = authResult.Item1; - success = authResult.Item2; - - if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) - { - user = await CreateUser(username).ConfigureAwait(false); - - var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy; - if (hasNewUserPolicy != null) - { - var policy = hasNewUserPolicy.GetNewUserPolicy(); - UpdateUserPolicy(user, policy, true); - } - } - } - - if (success && user != null && authenticationProvider != null) - { - var providerId = GetAuthenticationProviderId(authenticationProvider); - - if (!string.Equals(providerId, user.Policy.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) - { - user.Policy.AuthenticationProviderId = providerId; - UpdateUserPolicy(user, user.Policy, true); - } - } - - if (user == null) - { - throw new SecurityException("Invalid username or password entered."); - } - - if (user.Policy.IsDisabled) - { - throw new SecurityException(string.Format("The {0} account is currently disabled. Please consult with your administrator.", user.Name)); - } - - if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint)) - { - throw new SecurityException("Forbidden."); - } - - if (!user.IsParentalScheduleAllowed()) - { - throw new SecurityException("User is not allowed access at this time."); - } - - // Update LastActivityDate and LastLoginDate, then save - if (success) - { - if (isUserSession) - { - user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; - UpdateUser(user); - } - UpdateInvalidLoginAttemptCount(user, 0); - } - else - { - UpdateInvalidLoginAttemptCount(user, user.Policy.InvalidLoginAttemptCount + 1); - } - - _logger.LogInformation("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied"); - - return success ? user : null; - } - - private static string GetAuthenticationProviderId(IAuthenticationProvider provider) - { - return provider.GetType().FullName; - } - - private IAuthenticationProvider GetAuthenticationProvider(User user) - { - return GetAuthenticationProviders(user).First(); - } - - private IAuthenticationProvider[] GetAuthenticationProviders(User user) - { - var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId; - - var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray(); - - if (!string.IsNullOrEmpty(authenticationProviderId)) - { - providers = providers.Where(i => string.Equals(authenticationProviderId, GetAuthenticationProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray(); - } - - if (providers.Length == 0) - { - providers = new IAuthenticationProvider[] { _defaultAuthenticationProvider }; - } - - return providers; - } - - private async Task AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) - { - try - { - var requiresResolvedUser = provider as IRequiresResolvedUser; - if (requiresResolvedUser != null) - { - await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); - } - else - { - await provider.Authenticate(username, password).ConfigureAwait(false); - } - - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name); - - return false; - } - } - - private async Task> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) - { - bool success = false; - IAuthenticationProvider authenticationProvider = null; - - if (password != null && user != null) - { - // Doesn't look like this is even possible to be used, because of password == null checks below - hashedPassword = _defaultAuthenticationProvider.GetHashedString(user, password); - } - - if (password == null) - { - // legacy - success = string.Equals(_defaultAuthenticationProvider.GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); - } - else - { - foreach (var provider in GetAuthenticationProviders(user)) - { - success = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); - - if (success) - { - authenticationProvider = provider; - break; - } - } - } - - if (user != null) - { - if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword) - { - if (password == null) - { - // legacy - success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); - } - else - { - success = string.Equals(GetLocalPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); - } - } - } - - return new Tuple(authenticationProvider, success); - } - - private void UpdateInvalidLoginAttemptCount(User user, int newValue) - { - if (user.Policy.InvalidLoginAttemptCount == newValue || newValue <= 0) - { - return; - } - - user.Policy.InvalidLoginAttemptCount = newValue; - - var maxCount = user.Policy.IsAdministrator ? 3 : 5; - - var fireLockout = false; - - if (newValue >= maxCount) - { - _logger.LogDebug("Disabling user {0} due to {1} unsuccessful login attempts.", user.Name, newValue); - user.Policy.IsDisabled = true; - - fireLockout = true; - } - - UpdateUserPolicy(user, user.Policy, false); - - if (fireLockout) - { - UserLockedOut?.Invoke(this, new GenericEventArgs(user)); - } - } - - private string GetLocalPasswordHash(User user) - { - return string.IsNullOrEmpty(user.EasyPassword) - ? _defaultAuthenticationProvider.GetEmptyHashedString(user) - : user.EasyPassword; - } - - private bool IsPasswordEmpty(User user, string passwordHash) - { - return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase); - } - - /// - /// Loads the users from the repository - /// - /// IEnumerable{User}. - private User[] LoadUsers() - { - var users = UserRepository.RetrieveAllUsers(); - - // There always has to be at least one user. - if (users.Count == 0) - { - var defaultName = Environment.UserName; - if (string.IsNullOrWhiteSpace(defaultName)) - { - defaultName = "MyJellyfinUser"; - } - var name = MakeValidUsername(defaultName); - - var user = InstantiateNewUser(name); - - user.DateLastSaved = DateTime.UtcNow; - - UserRepository.CreateUser(user); - - users.Add(user); - - user.Policy.IsAdministrator = true; - user.Policy.EnableContentDeletion = true; - user.Policy.EnableRemoteControlOfOtherUsers = true; - UpdateUserPolicy(user, user.Policy, false); - } - - return users.ToArray(); - } - - public UserDto GetUserDto(User user, string remoteEndPoint = null) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; - var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user)); - - var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? - hasConfiguredEasyPassword : - hasConfiguredPassword; - - var dto = new UserDto - { - Id = user.Id, - Name = user.Name, - HasPassword = hasPassword, - HasConfiguredPassword = hasConfiguredPassword, - HasConfiguredEasyPassword = hasConfiguredEasyPassword, - LastActivityDate = user.LastActivityDate, - LastLoginDate = user.LastLoginDate, - Configuration = user.Configuration, - ServerId = _appHost.SystemId, - Policy = user.Policy - }; - - if (!hasPassword && Users.Count() == 1) - { - dto.EnableAutoLogin = true; - } - - var image = user.GetImageInfo(ImageType.Primary, 0); - - if (image != null) - { - dto.PrimaryImageTag = GetImageCacheTag(user, image); - - try - { - _dtoServiceFactory().AttachPrimaryImageAspectRatio(dto, user); - } - catch (Exception ex) - { - // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions - _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {user}", user.Name); - } - } - - return dto; - } - - public UserDto GetOfflineUserDto(User user) - { - var dto = GetUserDto(user); - - dto.ServerName = _appHost.FriendlyName; - - return dto; - } - - private string GetImageCacheTag(BaseItem item, ItemImageInfo image) - { - try - { - return _imageProcessorFactory().GetImageCacheTag(item, image); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting {imageType} image info for {imagePath}", image.Type, image.Path); - return null; - } - } - - /// - /// Refreshes metadata for each user - /// - /// The cancellation token. - /// Task. - public async Task RefreshUsersMetadata(CancellationToken cancellationToken) - { - foreach (var user in Users) - { - await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), cancellationToken).ConfigureAwait(false); - } - } - - /// - /// Renames the user. - /// - /// The user. - /// The new name. - /// Task. - /// user - /// - public async Task RenameUser(User user, string newName) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (string.IsNullOrEmpty(newName)) - { - throw new ArgumentNullException(nameof(newName)); - } - - if (Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) - { - throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName)); - } - - if (user.Name.Equals(newName, StringComparison.Ordinal)) - { - throw new ArgumentException("The new and old names must be different."); - } - - await user.Rename(newName); - - OnUserUpdated(user); - } - - /// - /// Updates the user. - /// - /// The user. - /// user - /// - public void UpdateUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (user.Id.Equals(Guid.Empty) || !Users.Any(u => u.Id.Equals(user.Id))) - { - throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id)); - } - - user.DateModified = DateTime.UtcNow; - user.DateLastSaved = DateTime.UtcNow; - - UserRepository.UpdateUser(user); - - OnUserUpdated(user); - } - - public event EventHandler> UserCreated; - - private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1); - - /// - /// Creates the user. - /// - /// The name. - /// User. - /// name - /// - public async Task CreateUser(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - if (!IsValidUsername(name)) - { - throw new ArgumentException("Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); - } - - if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) - { - throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name)); - } - - await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); - - try - { - var user = InstantiateNewUser(name); - - var list = Users.ToList(); - list.Add(user); - _users = list.ToArray(); - - user.DateLastSaved = DateTime.UtcNow; - - UserRepository.CreateUser(user); - - EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs { Argument = user }, _logger); - - return user; - } - finally - { - _userListLock.Release(); - } - } - - /// - /// Deletes the user. - /// - /// The user. - /// Task. - /// user - /// - public async Task DeleteUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - var allUsers = Users.ToList(); - - if (allUsers.FirstOrDefault(u => u.Id == user.Id) == null) - { - throw new ArgumentException(string.Format("The user cannot be deleted because there is no user with the Name {0} and Id {1}.", user.Name, user.Id)); - } - - if (allUsers.Count == 1) - { - throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one user in the system.", user.Name)); - } - - if (user.Policy.IsAdministrator && allUsers.Count(i => i.Policy.IsAdministrator) == 1) - { - throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one admin user in the system.", user.Name)); - } - - await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); - - try - { - var configPath = GetConfigurationFilePath(user); - - UserRepository.DeleteUser(user); - - try - { - _fileSystem.DeleteFile(configPath); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting file {path}", configPath); - } - - DeleteUserPolicy(user); - - _users = allUsers.Where(i => i.Id != user.Id).ToArray(); - - OnUserDeleted(user); - } - finally - { - _userListLock.Release(); - } - } - - /// - /// Resets the password by clearing it. - /// - /// Task. - public Task ResetPassword(User user) - { - return ChangePassword(user, string.Empty); - } - - public void ResetEasyPassword(User user) - { - ChangeEasyPassword(user, string.Empty, null); - } - - public async Task ChangePassword(User user, string newPassword) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); - - UpdateUser(user); - - UserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); - } - - public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (newPassword != null) - { - newPasswordHash = _defaultAuthenticationProvider.GetHashedString(user, newPassword); - } - - if (string.IsNullOrWhiteSpace(newPasswordHash)) - { - throw new ArgumentNullException(nameof(newPasswordHash)); - } - - user.EasyPassword = newPasswordHash; - - UpdateUser(user); - - UserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); - } - - /// - /// Instantiates the new user. - /// - /// The name. - /// User. - private static User InstantiateNewUser(string name) - { - return new User - { - Name = name, - Id = Guid.NewGuid(), - DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, - UsesIdForConfigurationPath = true, - //Salt = BCrypt.GenerateSalt() - }; - } - - private string PasswordResetFile => Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "passwordreset.txt"); - - private string _lastPin; - private PasswordPinCreationResult _lastPasswordPinCreationResult; - private int _pinAttempts; - - private async Task CreatePasswordResetPin() - { - var num = new Random().Next(1, 9999); - - var path = PasswordResetFile; - - var pin = num.ToString("0000", CultureInfo.InvariantCulture); - _lastPin = pin; - - var time = TimeSpan.FromMinutes(5); - var expiration = DateTime.UtcNow.Add(time); - - var text = new StringBuilder(); - - var localAddress = (await _appHost.GetLocalApiUrl(CancellationToken.None).ConfigureAwait(false)) ?? string.Empty; - - text.AppendLine("Use your web browser to visit:"); - text.AppendLine(string.Empty); - text.AppendLine(localAddress + "/web/index.html#!/forgotpasswordpin.html"); - text.AppendLine(string.Empty); - text.AppendLine("Enter the following pin code:"); - text.AppendLine(string.Empty); - text.AppendLine(pin); - text.AppendLine(string.Empty); - - var localExpirationTime = expiration.ToLocalTime(); - // Tuesday, 22 August 2006 06:30 AM - text.AppendLine("The pin code will expire at " + localExpirationTime.ToString("f1", CultureInfo.CurrentCulture)); - - File.WriteAllText(path, text.ToString(), Encoding.UTF8); - - var result = new PasswordPinCreationResult - { - PinFile = path, - ExpirationDate = expiration - }; - - _lastPasswordPinCreationResult = result; - _pinAttempts = 0; - - return result; - } - - public async Task StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) - { - DeletePinFile(); - - var user = string.IsNullOrWhiteSpace(enteredUsername) ? - null : - GetUserByName(enteredUsername); - - var action = ForgotPasswordAction.InNetworkRequired; - string pinFile = null; - DateTime? expirationDate = null; - - if (user != null && !user.Policy.IsAdministrator) - { - action = ForgotPasswordAction.ContactAdmin; - } - else - { - if (isInNetwork) - { - action = ForgotPasswordAction.PinCode; - } - - var result = await CreatePasswordResetPin().ConfigureAwait(false); - pinFile = result.PinFile; - expirationDate = result.ExpirationDate; - } - - return new ForgotPasswordResult - { - Action = action, - PinFile = pinFile, - PinExpirationDate = expirationDate - }; - } - - public async Task RedeemPasswordResetPin(string pin) - { - DeletePinFile(); - - var usersReset = new List(); - - var valid = !string.IsNullOrWhiteSpace(_lastPin) && - string.Equals(_lastPin, pin, StringComparison.OrdinalIgnoreCase) && - _lastPasswordPinCreationResult != null && - _lastPasswordPinCreationResult.ExpirationDate > DateTime.UtcNow; - - if (valid) - { - _lastPin = null; - _lastPasswordPinCreationResult = null; - - foreach (var user in Users) - { - await ResetPassword(user).ConfigureAwait(false); - - if (user.Policy.IsDisabled) - { - user.Policy.IsDisabled = false; - UpdateUserPolicy(user, user.Policy, true); - } - usersReset.Add(user.Name); - } - } - else - { - _pinAttempts++; - if (_pinAttempts >= 3) - { - _lastPin = null; - _lastPasswordPinCreationResult = null; - } - } - - return new PinRedeemResult - { - Success = valid, - UsersReset = usersReset.ToArray() - }; - } - - private void DeletePinFile() - { - try - { - _fileSystem.DeleteFile(PasswordResetFile); - } - catch - { - - } - } - - class PasswordPinCreationResult - { - public string PinFile { get; set; } - public DateTime ExpirationDate { get; set; } - } - - public UserPolicy GetUserPolicy(User user) - { - var path = GetPolicyFilePath(user); - - if (!File.Exists(path)) - { - return GetDefaultPolicy(user); - } - - try - { - lock (_policySyncLock) - { - return (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), path); - } - } - catch (IOException) - { - return GetDefaultPolicy(user); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reading policy file: {path}", path); - - return GetDefaultPolicy(user); - } - } - - private static UserPolicy GetDefaultPolicy(User user) - { - return new UserPolicy - { - EnableContentDownloading = true, - EnableSyncTranscoding = true - }; - } - - private readonly object _policySyncLock = new object(); - public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy) - { - var user = GetUserById(userId); - UpdateUserPolicy(user, userPolicy, true); - } - - private void UpdateUserPolicy(User user, UserPolicy userPolicy, bool fireEvent) - { - // The xml serializer will output differently if the type is not exact - if (userPolicy.GetType() != typeof(UserPolicy)) - { - var json = _jsonSerializer.SerializeToString(userPolicy); - userPolicy = _jsonSerializer.DeserializeFromString(json); - } - - var path = GetPolicyFilePath(user); - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - lock (_policySyncLock) - { - _xmlSerializer.SerializeToFile(userPolicy, path); - user.Policy = userPolicy; - } - - if (fireEvent) - { - UserPolicyUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); - } - } - - private void DeleteUserPolicy(User user) - { - var path = GetPolicyFilePath(user); - - try - { - lock (_policySyncLock) - { - _fileSystem.DeleteFile(path); - } - } - catch (IOException) - { - - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting policy file"); - } - } - - private static string GetPolicyFilePath(User user) - { - return Path.Combine(user.ConfigurationDirectoryPath, "policy.xml"); - } - - private static string GetConfigurationFilePath(User user) - { - return Path.Combine(user.ConfigurationDirectoryPath, "config.xml"); - } - - public UserConfiguration GetUserConfiguration(User user) - { - var path = GetConfigurationFilePath(user); - - if (!File.Exists(path)) - { - return new UserConfiguration(); - } - - try - { - lock (_configSyncLock) - { - return (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), path); - } - } - catch (IOException) - { - return new UserConfiguration(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reading policy file: {path}", path); - - return new UserConfiguration(); - } - } - - private readonly object _configSyncLock = new object(); - public void UpdateConfiguration(Guid userId, UserConfiguration config) - { - var user = GetUserById(userId); - UpdateConfiguration(user, config); - } - - public void UpdateConfiguration(User user, UserConfiguration config) - { - UpdateConfiguration(user, config, true); - } - - private void UpdateConfiguration(User user, UserConfiguration config, bool fireEvent) - { - var path = GetConfigurationFilePath(user); - - // The xml serializer will output differently if the type is not exact - if (config.GetType() != typeof(UserConfiguration)) - { - var json = _jsonSerializer.SerializeToString(config); - config = _jsonSerializer.DeserializeFromString(json); - } - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - lock (_configSyncLock) - { - _xmlSerializer.SerializeToFile(config, path); - user.Configuration = config; - } - - if (fireEvent) - { - UserConfigurationUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); - } - } - } - - public class DeviceAccessEntryPoint : IServerEntryPoint - { - private IUserManager _userManager; - private IAuthenticationRepository _authRepo; - private IDeviceManager _deviceManager; - private ISessionManager _sessionManager; - - public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager) - { - _userManager = userManager; - _authRepo = authRepo; - _deviceManager = deviceManager; - _sessionManager = sessionManager; - } - - public Task RunAsync() - { - _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated; - - return Task.CompletedTask; - } - - private void _userManager_UserPolicyUpdated(object sender, GenericEventArgs e) - { - var user = e.Argument; - if (!user.Policy.EnableAllDevices) - { - UpdateDeviceAccess(user); - } - } - - private void UpdateDeviceAccess(User user) - { - var existing = _authRepo.Get(new AuthenticationInfoQuery - { - UserId = user.Id - - }).Items; - - foreach (var authInfo in existing) - { - if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId)) - { - _sessionManager.Logout(authInfo); - } - } - } - - public void Dispose() - { - - } - } -} + } + + public string MakeValidUsername(string username) + { + if (IsValidUsername(username)) + { + return username; + } + + // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) + var builder = new StringBuilder(); + + foreach (var c in username) + { + if (IsValidUsernameCharacter(c)) + { + builder.Append(c); + } + } + return builder.ToString(); + } + + public async Task AuthenticateUser(string username, string password, string hashedPassword, string remoteEndPoint, bool isUserSession) + { + if (string.IsNullOrWhiteSpace(username)) + { + throw new ArgumentNullException(nameof(username)); + } + + var user = Users + .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); + + var success = false; + IAuthenticationProvider authenticationProvider = null; + + if (user != null) + { + var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false); + authenticationProvider = authResult.Item1; + success = authResult.Item2; + } + else + { + // user is null + var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); + authenticationProvider = authResult.Item1; + success = authResult.Item2; + + if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) + { + user = await CreateUser(username).ConfigureAwait(false); + + var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy; + if (hasNewUserPolicy != null) + { + var policy = hasNewUserPolicy.GetNewUserPolicy(); + UpdateUserPolicy(user, policy, true); + } + } + } + + if (success && user != null && authenticationProvider != null) + { + var providerId = GetAuthenticationProviderId(authenticationProvider); + + if (!string.Equals(providerId, user.Policy.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) + { + user.Policy.AuthenticationProviderId = providerId; + UpdateUserPolicy(user, user.Policy, true); + } + } + + if (user == null) + { + throw new SecurityException("Invalid username or password entered."); + } + + if (user.Policy.IsDisabled) + { + throw new SecurityException(string.Format("The {0} account is currently disabled. Please consult with your administrator.", user.Name)); + } + + if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint)) + { + throw new SecurityException("Forbidden."); + } + + if (!user.IsParentalScheduleAllowed()) + { + throw new SecurityException("User is not allowed access at this time."); + } + + // Update LastActivityDate and LastLoginDate, then save + if (success) + { + if (isUserSession) + { + user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; + UpdateUser(user); + } + UpdateInvalidLoginAttemptCount(user, 0); + } + else + { + UpdateInvalidLoginAttemptCount(user, user.Policy.InvalidLoginAttemptCount + 1); + } + + _logger.LogInformation("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied"); + + return success ? user : null; + } + + private static string GetAuthenticationProviderId(IAuthenticationProvider provider) + { + return provider.GetType().FullName; + } + + private IAuthenticationProvider GetAuthenticationProvider(User user) + { + return GetAuthenticationProviders(user).First(); + } + + private IAuthenticationProvider[] GetAuthenticationProviders(User user) + { + var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId; + + var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray(); + + if (!string.IsNullOrEmpty(authenticationProviderId)) + { + providers = providers.Where(i => string.Equals(authenticationProviderId, GetAuthenticationProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray(); + } + + if (providers.Length == 0) + { + providers = new IAuthenticationProvider[] { _defaultAuthenticationProvider }; + } + + return providers; + } + + private async Task AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) + { + try + { + var requiresResolvedUser = provider as IRequiresResolvedUser; + if (requiresResolvedUser != null) + { + await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); + } + else + { + await provider.Authenticate(username, password).ConfigureAwait(false); + } + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name); + + return false; + } + } + + private async Task> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) + { + bool success = false; + IAuthenticationProvider authenticationProvider = null; + + if (password != null && user != null) + { + // Doesn't look like this is even possible to be used, because of password == null checks below + hashedPassword = _defaultAuthenticationProvider.GetHashedString(user, password); + } + + if (password == null) + { + // legacy + success = string.Equals(_defaultAuthenticationProvider.GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + } + else + { + foreach (var provider in GetAuthenticationProviders(user)) + { + success = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); + + if (success) + { + authenticationProvider = provider; + break; + } + } + } + + if (user != null) + { + if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword) + { + if (password == null) + { + // legacy + success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + } + else + { + success = string.Equals(GetLocalPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); + } + } + } + + return new Tuple(authenticationProvider, success); + } + + private void UpdateInvalidLoginAttemptCount(User user, int newValue) + { + if (user.Policy.InvalidLoginAttemptCount == newValue || newValue <= 0) + { + return; + } + + user.Policy.InvalidLoginAttemptCount = newValue; + + var maxCount = user.Policy.IsAdministrator ? 3 : 5; + + var fireLockout = false; + + if (newValue >= maxCount) + { + _logger.LogDebug("Disabling user {0} due to {1} unsuccessful login attempts.", user.Name, newValue); + user.Policy.IsDisabled = true; + + fireLockout = true; + } + + UpdateUserPolicy(user, user.Policy, false); + + if (fireLockout) + { + UserLockedOut?.Invoke(this, new GenericEventArgs(user)); + } + } + + private string GetLocalPasswordHash(User user) + { + return string.IsNullOrEmpty(user.EasyPassword) + ? _defaultAuthenticationProvider.GetEmptyHashedString(user) + : user.EasyPassword; + } + + private bool IsPasswordEmpty(User user, string passwordHash) + { + return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase); + } + + /// + /// Loads the users from the repository + /// + /// IEnumerable{User}. + private User[] LoadUsers() + { + var users = UserRepository.RetrieveAllUsers(); + + // There always has to be at least one user. + if (users.Count == 0) + { + var defaultName = Environment.UserName; + if (string.IsNullOrWhiteSpace(defaultName)) + { + defaultName = "MyJellyfinUser"; + } + var name = MakeValidUsername(defaultName); + + var user = InstantiateNewUser(name); + + user.DateLastSaved = DateTime.UtcNow; + + UserRepository.CreateUser(user); + + users.Add(user); + + user.Policy.IsAdministrator = true; + user.Policy.EnableContentDeletion = true; + user.Policy.EnableRemoteControlOfOtherUsers = true; + UpdateUserPolicy(user, user.Policy, false); + } + + return users.ToArray(); + } + + public UserDto GetUserDto(User user, string remoteEndPoint = null) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; + var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user)); + + var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? + hasConfiguredEasyPassword : + hasConfiguredPassword; + + var dto = new UserDto + { + Id = user.Id, + Name = user.Name, + HasPassword = hasPassword, + HasConfiguredPassword = hasConfiguredPassword, + HasConfiguredEasyPassword = hasConfiguredEasyPassword, + LastActivityDate = user.LastActivityDate, + LastLoginDate = user.LastLoginDate, + Configuration = user.Configuration, + ServerId = _appHost.SystemId, + Policy = user.Policy + }; + + if (!hasPassword && Users.Count() == 1) + { + dto.EnableAutoLogin = true; + } + + var image = user.GetImageInfo(ImageType.Primary, 0); + + if (image != null) + { + dto.PrimaryImageTag = GetImageCacheTag(user, image); + + try + { + _dtoServiceFactory().AttachPrimaryImageAspectRatio(dto, user); + } + catch (Exception ex) + { + // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions + _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {user}", user.Name); + } + } + + return dto; + } + + public UserDto GetOfflineUserDto(User user) + { + var dto = GetUserDto(user); + + dto.ServerName = _appHost.FriendlyName; + + return dto; + } + + private string GetImageCacheTag(BaseItem item, ItemImageInfo image) + { + try + { + return _imageProcessorFactory().GetImageCacheTag(item, image); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting {imageType} image info for {imagePath}", image.Type, image.Path); + return null; + } + } + + /// + /// Refreshes metadata for each user + /// + /// The cancellation token. + /// Task. + public async Task RefreshUsersMetadata(CancellationToken cancellationToken) + { + foreach (var user in Users) + { + await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Renames the user. + /// + /// The user. + /// The new name. + /// Task. + /// user + /// + public async Task RenameUser(User user, string newName) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (string.IsNullOrEmpty(newName)) + { + throw new ArgumentNullException(nameof(newName)); + } + + if (Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName)); + } + + if (user.Name.Equals(newName, StringComparison.Ordinal)) + { + throw new ArgumentException("The new and old names must be different."); + } + + await user.Rename(newName); + + OnUserUpdated(user); + } + + /// + /// Updates the user. + /// + /// The user. + /// user + /// + public void UpdateUser(User user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.Id.Equals(Guid.Empty) || !Users.Any(u => u.Id.Equals(user.Id))) + { + throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id)); + } + + user.DateModified = DateTime.UtcNow; + user.DateLastSaved = DateTime.UtcNow; + + UserRepository.UpdateUser(user); + + OnUserUpdated(user); + } + + public event EventHandler> UserCreated; + + private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1); + + /// + /// Creates the user. + /// + /// The name. + /// User. + /// name + /// + public async Task CreateUser(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + if (!IsValidUsername(name)) + { + throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); + } + + if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name)); + } + + await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); + + try + { + var user = InstantiateNewUser(name); + + var list = Users.ToList(); + list.Add(user); + _users = list.ToArray(); + + user.DateLastSaved = DateTime.UtcNow; + + UserRepository.CreateUser(user); + + EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs { Argument = user }, _logger); + + return user; + } + finally + { + _userListLock.Release(); + } + } + + /// + /// Deletes the user. + /// + /// The user. + /// Task. + /// user + /// + public async Task DeleteUser(User user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var allUsers = Users.ToList(); + + if (allUsers.FirstOrDefault(u => u.Id == user.Id) == null) + { + throw new ArgumentException(string.Format("The user cannot be deleted because there is no user with the Name {0} and Id {1}.", user.Name, user.Id)); + } + + if (allUsers.Count == 1) + { + throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one user in the system.", user.Name)); + } + + if (user.Policy.IsAdministrator && allUsers.Count(i => i.Policy.IsAdministrator) == 1) + { + throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one admin user in the system.", user.Name)); + } + + await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); + + try + { + var configPath = GetConfigurationFilePath(user); + + UserRepository.DeleteUser(user); + + try + { + _fileSystem.DeleteFile(configPath); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting file {path}", configPath); + } + + DeleteUserPolicy(user); + + _users = allUsers.Where(i => i.Id != user.Id).ToArray(); + + OnUserDeleted(user); + } + finally + { + _userListLock.Release(); + } + } + + /// + /// Resets the password by clearing it. + /// + /// Task. + public Task ResetPassword(User user) + { + return ChangePassword(user, string.Empty); + } + + public void ResetEasyPassword(User user) + { + ChangeEasyPassword(user, string.Empty, null); + } + + public async Task ChangePassword(User user, string newPassword) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); + + UpdateUser(user); + + UserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); + } + + public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (newPassword != null) + { + newPasswordHash = _defaultAuthenticationProvider.GetHashedString(user, newPassword); + } + + if (string.IsNullOrWhiteSpace(newPasswordHash)) + { + throw new ArgumentNullException(nameof(newPasswordHash)); + } + + user.EasyPassword = newPasswordHash; + + UpdateUser(user); + + UserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); + } + + /// + /// Instantiates the new user. + /// + /// The name. + /// User. + private static User InstantiateNewUser(string name) + { + return new User + { + Name = name, + Id = Guid.NewGuid(), + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + UsesIdForConfigurationPath = true, + //Salt = BCrypt.GenerateSalt() + }; + } + + private string PasswordResetFile => Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "passwordreset.txt"); + + private string _lastPin; + private PasswordPinCreationResult _lastPasswordPinCreationResult; + private int _pinAttempts; + + private async Task CreatePasswordResetPin() + { + var num = new Random().Next(1, 9999); + + var path = PasswordResetFile; + + var pin = num.ToString("0000", CultureInfo.InvariantCulture); + _lastPin = pin; + + var time = TimeSpan.FromMinutes(5); + var expiration = DateTime.UtcNow.Add(time); + + var text = new StringBuilder(); + + var localAddress = (await _appHost.GetLocalApiUrl(CancellationToken.None).ConfigureAwait(false)) ?? string.Empty; + + text.AppendLine("Use your web browser to visit:"); + text.AppendLine(string.Empty); + text.AppendLine(localAddress + "/web/index.html#!/forgotpasswordpin.html"); + text.AppendLine(string.Empty); + text.AppendLine("Enter the following pin code:"); + text.AppendLine(string.Empty); + text.AppendLine(pin); + text.AppendLine(string.Empty); + + var localExpirationTime = expiration.ToLocalTime(); + // Tuesday, 22 August 2006 06:30 AM + text.AppendLine("The pin code will expire at " + localExpirationTime.ToString("f1", CultureInfo.CurrentCulture)); + + File.WriteAllText(path, text.ToString(), Encoding.UTF8); + + var result = new PasswordPinCreationResult + { + PinFile = path, + ExpirationDate = expiration + }; + + _lastPasswordPinCreationResult = result; + _pinAttempts = 0; + + return result; + } + + public async Task StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) + { + DeletePinFile(); + + var user = string.IsNullOrWhiteSpace(enteredUsername) ? + null : + GetUserByName(enteredUsername); + + var action = ForgotPasswordAction.InNetworkRequired; + string pinFile = null; + DateTime? expirationDate = null; + + if (user != null && !user.Policy.IsAdministrator) + { + action = ForgotPasswordAction.ContactAdmin; + } + else + { + if (isInNetwork) + { + action = ForgotPasswordAction.PinCode; + } + + var result = await CreatePasswordResetPin().ConfigureAwait(false); + pinFile = result.PinFile; + expirationDate = result.ExpirationDate; + } + + return new ForgotPasswordResult + { + Action = action, + PinFile = pinFile, + PinExpirationDate = expirationDate + }; + } + + public async Task RedeemPasswordResetPin(string pin) + { + DeletePinFile(); + + var usersReset = new List(); + + var valid = !string.IsNullOrWhiteSpace(_lastPin) && + string.Equals(_lastPin, pin, StringComparison.OrdinalIgnoreCase) && + _lastPasswordPinCreationResult != null && + _lastPasswordPinCreationResult.ExpirationDate > DateTime.UtcNow; + + if (valid) + { + _lastPin = null; + _lastPasswordPinCreationResult = null; + + foreach (var user in Users) + { + await ResetPassword(user).ConfigureAwait(false); + + if (user.Policy.IsDisabled) + { + user.Policy.IsDisabled = false; + UpdateUserPolicy(user, user.Policy, true); + } + usersReset.Add(user.Name); + } + } + else + { + _pinAttempts++; + if (_pinAttempts >= 3) + { + _lastPin = null; + _lastPasswordPinCreationResult = null; + } + } + + return new PinRedeemResult + { + Success = valid, + UsersReset = usersReset.ToArray() + }; + } + + private void DeletePinFile() + { + try + { + _fileSystem.DeleteFile(PasswordResetFile); + } + catch + { + + } + } + + class PasswordPinCreationResult + { + public string PinFile { get; set; } + public DateTime ExpirationDate { get; set; } + } + + public UserPolicy GetUserPolicy(User user) + { + var path = GetPolicyFilePath(user); + + if (!File.Exists(path)) + { + return GetDefaultPolicy(user); + } + + try + { + lock (_policySyncLock) + { + return (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), path); + } + } + catch (IOException) + { + return GetDefaultPolicy(user); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error reading policy file: {path}", path); + + return GetDefaultPolicy(user); + } + } + + private static UserPolicy GetDefaultPolicy(User user) + { + return new UserPolicy + { + EnableContentDownloading = true, + EnableSyncTranscoding = true + }; + } + + private readonly object _policySyncLock = new object(); + public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy) + { + var user = GetUserById(userId); + UpdateUserPolicy(user, userPolicy, true); + } + + private void UpdateUserPolicy(User user, UserPolicy userPolicy, bool fireEvent) + { + // The xml serializer will output differently if the type is not exact + if (userPolicy.GetType() != typeof(UserPolicy)) + { + var json = _jsonSerializer.SerializeToString(userPolicy); + userPolicy = _jsonSerializer.DeserializeFromString(json); + } + + var path = GetPolicyFilePath(user); + + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + lock (_policySyncLock) + { + _xmlSerializer.SerializeToFile(userPolicy, path); + user.Policy = userPolicy; + } + + if (fireEvent) + { + UserPolicyUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); + } + } + + private void DeleteUserPolicy(User user) + { + var path = GetPolicyFilePath(user); + + try + { + lock (_policySyncLock) + { + _fileSystem.DeleteFile(path); + } + } + catch (IOException) + { + + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting policy file"); + } + } + + private static string GetPolicyFilePath(User user) + { + return Path.Combine(user.ConfigurationDirectoryPath, "policy.xml"); + } + + private static string GetConfigurationFilePath(User user) + { + return Path.Combine(user.ConfigurationDirectoryPath, "config.xml"); + } + + public UserConfiguration GetUserConfiguration(User user) + { + var path = GetConfigurationFilePath(user); + + if (!File.Exists(path)) + { + return new UserConfiguration(); + } + + try + { + lock (_configSyncLock) + { + return (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), path); + } + } + catch (IOException) + { + return new UserConfiguration(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error reading policy file: {path}", path); + + return new UserConfiguration(); + } + } + + private readonly object _configSyncLock = new object(); + public void UpdateConfiguration(Guid userId, UserConfiguration config) + { + var user = GetUserById(userId); + UpdateConfiguration(user, config); + } + + public void UpdateConfiguration(User user, UserConfiguration config) + { + UpdateConfiguration(user, config, true); + } + + private void UpdateConfiguration(User user, UserConfiguration config, bool fireEvent) + { + var path = GetConfigurationFilePath(user); + + // The xml serializer will output differently if the type is not exact + if (config.GetType() != typeof(UserConfiguration)) + { + var json = _jsonSerializer.SerializeToString(config); + config = _jsonSerializer.DeserializeFromString(json); + } + + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + lock (_configSyncLock) + { + _xmlSerializer.SerializeToFile(config, path); + user.Configuration = config; + } + + if (fireEvent) + { + UserConfigurationUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); + } + } + } + + public class DeviceAccessEntryPoint : IServerEntryPoint + { + private IUserManager _userManager; + private IAuthenticationRepository _authRepo; + private IDeviceManager _deviceManager; + private ISessionManager _sessionManager; + + public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager) + { + _userManager = userManager; + _authRepo = authRepo; + _deviceManager = deviceManager; + _sessionManager = sessionManager; + } + + public Task RunAsync() + { + _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated; + + return Task.CompletedTask; + } + + private void _userManager_UserPolicyUpdated(object sender, GenericEventArgs e) + { + var user = e.Argument; + if (!user.Policy.EnableAllDevices) + { + UpdateDeviceAccess(user); + } + } + + private void UpdateDeviceAccess(User user) + { + var existing = _authRepo.Get(new AuthenticationInfoQuery + { + UserId = user.Id + + }).Items; + + foreach (var authInfo in existing) + { + if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId)) + { + _sessionManager.Logout(authInfo); + } + } + } + + public void Dispose() + { + + } + } +} diff --git a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs index ec7e57fec..8accc696e 100644 --- a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs +++ b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs @@ -1,15 +1,15 @@ -using System; -using System.IO; -using System.Collections.Generic; - -namespace MediaBrowser.Model.Cryptography -{ - public interface ICryptoProvider - { - Guid GetMD5(string str); - byte[] ComputeMD5(Stream str); - byte[] ComputeMD5(byte[] bytes); - byte[] ComputeSHA1(byte[] bytes); +using System; +using System.IO; +using System.Collections.Generic; + +namespace MediaBrowser.Model.Cryptography +{ + public interface ICryptoProvider + { + Guid GetMD5(string str); + byte[] ComputeMD5(Stream str); + byte[] ComputeMD5(byte[] bytes); + byte[] ComputeSHA1(byte[] bytes); IEnumerable GetSupportedHashMethods(); byte[] ComputeHash(string HashMethod, byte[] bytes); byte[] ComputeHashWithDefaultMethod(byte[] bytes); @@ -17,5 +17,6 @@ namespace MediaBrowser.Model.Cryptography byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt); byte[] ComputeHash(PasswordHash hash); byte[] GenerateSalt(); - } -} + string DefaultHashMethod { get; } + } +} diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs index d37220ab2..524484b10 100644 --- a/MediaBrowser.Model/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs @@ -33,15 +33,15 @@ namespace MediaBrowser.Model.Cryptography if (a.Length == 4) { Salt = a[2]; - SaltBytes = Convert.FromBase64CharArray(Salt.ToCharArray(), 0, Salt.Length); + SaltBytes = FromByteString(Salt); Hash = a[3]; - HashBytes = Convert.FromBase64CharArray(Hash.ToCharArray(), 0, Hash.Length); + HashBytes = FromByteString(Hash); } else { Salt = string.Empty; Hash = a[3]; - HashBytes = Convert.FromBase64CharArray(Hash.ToCharArray(), 0, Hash.Length); + HashBytes = FromByteString(Hash); } } else @@ -49,15 +49,15 @@ namespace MediaBrowser.Model.Cryptography if (a.Length == 4) { Salt = a[2]; - SaltBytes = Convert.FromBase64CharArray(Salt.ToCharArray(), 0, Salt.Length); + SaltBytes = FromByteString(Salt); Hash = a[3]; - HashBytes = Convert.FromBase64CharArray(Hash.ToCharArray(), 0, Hash.Length); + HashBytes = FromByteString(Hash); } else { Salt = string.Empty; Hash = a[2]; - HashBytes = Convert.FromBase64CharArray(Hash.ToCharArray(), 0, Hash.Length); + HashBytes = FromByteString(Hash); } } @@ -68,7 +68,17 @@ namespace MediaBrowser.Model.Cryptography { Id = "SHA256"; SaltBytes = cryptoProvider2.GenerateSalt(); - Salt = Convert.ToBase64String(SaltBytes); + Salt = BitConverter.ToString(SaltBytes).Replace("-", ""); + } + + private byte[] FromByteString(string ByteString) + { + List Bytes = new List(); + for (int i = 0; i < ByteString.Length; i += 2) + { + Bytes.Add(Convert.ToByte(ByteString.Substring(i, 2),16)); + } + return Bytes.ToArray(); } private string SerializeParameters() { @@ -77,7 +87,7 @@ namespace MediaBrowser.Model.Cryptography { ReturnString += String.Format(",{0}={1}", KVP.Key, KVP.Value); } - if (ReturnString[0] == ',') + if ((!string.IsNullOrEmpty(ReturnString)) && ReturnString[0] == ',') { ReturnString = ReturnString.Remove(0, 1); } @@ -85,8 +95,15 @@ namespace MediaBrowser.Model.Cryptography } public override string ToString() - { - return String.Format("${0}${1}${2}${3}", Id, SerializeParameters(), Salt, Hash); + { + string OutString = "$"; + OutString += Id; + if (!string.IsNullOrEmpty(SerializeParameters())) + OutString += $"${SerializeParameters()}"; + if (!string.IsNullOrEmpty(Salt)) + OutString += $"${Salt}"; + OutString += $"${Hash}"; + return OutString; } } -- cgit v1.2.3 From 77602aff889e605f8178ecf95592c0d75102e59f Mon Sep 17 00:00:00 2001 From: Phallacy Date: Wed, 13 Feb 2019 00:33:00 -0800 Subject: Minor fixes re:PR870, added null checks from PR876 --- .../Cryptography/CryptographyProvider.cs | 38 +++++++---- .../Data/SqliteUserRepository.cs | 32 +++++++++ .../Library/DefaultAuthenticationProvider.cs | 71 +++++++------------- Emby.Server.Implementations/Library/UserManager.cs | 6 +- MediaBrowser.Model/Cryptography/PasswordHash.cs | 75 +++++++++++++--------- 5 files changed, 124 insertions(+), 98 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 4f2bc1b03..7817989e7 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Cryptography { public class CryptographyProvider : ICryptoProvider { - private List SupportedHashMethods = new List(); + private HashSet SupportedHashMethods; public string DefaultHashMethod => "SHA256"; private RandomNumberGenerator rng; private int defaultiterations = 1000; @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Cryptography { //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 - SupportedHashMethods = new List + SupportedHashMethods = new HashSet() { "MD5" ,"System.Security.Cryptography.MD5" @@ -71,9 +71,9 @@ namespace Emby.Server.Implementations.Cryptography return SupportedHashMethods; } - private byte[] PBKDF2(string method, byte[] bytes, byte[] salt) - { - using (var r = new Rfc2898DeriveBytes(bytes, salt, defaultiterations, new HashAlgorithmName(method))) + private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations) + { + using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations, new HashAlgorithmName(method))) { return r.GetBytes(32); } @@ -102,30 +102,40 @@ namespace Emby.Server.Implementations.Cryptography } else { - return PBKDF2(HashMethod, bytes, salt); + return PBKDF2(HashMethod, bytes, salt,defaultiterations); } } else { throw new CryptographicException(String.Format("Requested hash method is not supported: {0}", HashMethod)); } - } + } public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt) { - return PBKDF2(DefaultHashMethod, bytes, salt); + return PBKDF2(DefaultHashMethod, bytes, salt, defaultiterations); } public byte[] ComputeHash(PasswordHash hash) - { - return ComputeHash(hash.Id, hash.HashBytes, hash.SaltBytes); - } - + { + int iterations = defaultiterations; + if (!hash.Parameters.ContainsKey("iterations")) + { + hash.Parameters.Add("iterations", defaultiterations.ToString()); + } + else + { + try { iterations = int.Parse(hash.Parameters["iterations"]); } + catch (Exception e) { iterations = defaultiterations; throw new Exception($"Couldn't successfully parse iterations value from string:{hash.Parameters["iterations"]}", e); } + } + return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes,iterations); + } + public byte[] GenerateSalt() { - byte[] salt = new byte[8]; + byte[] salt = new byte[64]; rng.GetBytes(salt); return salt; - } + } } } diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs index db359d7dd..b3d457342 100644 --- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs @@ -55,6 +55,7 @@ namespace Emby.Server.Implementations.Data { TryMigrateToLocalUsersTable(connection); } + RemoveEmptyPasswordHashes(); } } @@ -73,6 +74,37 @@ namespace Emby.Server.Implementations.Data } } + private void RemoveEmptyPasswordHashes() + { + foreach (var user in RetrieveAllUsers()) + { + // If the user password is the sha1 hash of the empty string, remove it + if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709") || !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709")) + { + continue; + } + + user.Password = null; + var serialized = _jsonSerializer.SerializeToBytes(user); + + using (WriteLock.Write()) + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId")) + { + statement.TryBind("@InternalId", user.InternalId); + statement.TryBind("@data", serialized); + statement.MoveNext(); + } + + }, TransactionMode); + } + } + + } + /// /// Save a user in the repo /// diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 255fd8252..ca6217016 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -36,32 +36,27 @@ namespace Emby.Server.Implementations.Library bool success = false; if (resolvedUser == null) { - success = false; throw new Exception("Invalid username or password"); } ConvertPasswordFormat(resolvedUser); byte[] passwordbytes = Encoding.UTF8.GetBytes(password); - - if (!resolvedUser.Password.Contains("$")) - { - ConvertPasswordFormat(resolvedUser); - } - PasswordHash ReadyHash = new PasswordHash(resolvedUser.Password); + + PasswordHash readyHash = new PasswordHash(resolvedUser.Password); byte[] CalculatedHash; string CalculatedHashString; - if (_cryptographyProvider.GetSupportedHashMethods().Any(i => i == ReadyHash.Id)) + if (_cryptographyProvider.GetSupportedHashMethods().Any(i => i == readyHash.Id)) { - if (String.IsNullOrEmpty(ReadyHash.Salt)) + if (String.IsNullOrEmpty(readyHash.Salt)) { - CalculatedHash = _cryptographyProvider.ComputeHash(ReadyHash.Id, passwordbytes); + CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes); CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); } else { - CalculatedHash = _cryptographyProvider.ComputeHash(ReadyHash.Id, passwordbytes, ReadyHash.SaltBytes); + CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes); CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); } - if (CalculatedHashString == ReadyHash.Hash) + if (CalculatedHashString == readyHash.Hash) { success = true; //throw new Exception("Invalid username or password"); @@ -69,8 +64,7 @@ namespace Emby.Server.Implementations.Library } else { - success = false; - throw new Exception(String.Format("Requested crypto method not available in provider: {0}", ReadyHash.Id)); + throw new Exception(String.Format("Requested crypto method not available in provider: {0}", readyHash.Id)); } //var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); @@ -105,26 +99,6 @@ namespace Emby.Server.Implementations.Library } } - // OLD VERSION //public Task Authenticate(string username, string password, User resolvedUser) - // OLD VERSION //{ - // OLD VERSION // if (resolvedUser == null) - // OLD VERSION // { - // OLD VERSION // throw new Exception("Invalid username or password"); - // OLD VERSION // } - // OLD VERSION // - // OLD VERSION // var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); - // OLD VERSION // - // OLD VERSION // if (!success) - // OLD VERSION // { - // OLD VERSION // throw new Exception("Invalid username or password"); - // OLD VERSION // } - // OLD VERSION // - // OLD VERSION // return Task.FromResult(new ProviderAuthenticationResult - // OLD VERSION // { - // OLD VERSION // Username = username - // OLD VERSION // }); - // OLD VERSION //} - public Task HasPassword(User user) { var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user)); @@ -133,7 +107,7 @@ namespace Emby.Server.Implementations.Library private bool IsPasswordEmpty(User user, string passwordHash) { - return string.Equals(passwordHash, GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase); + return string.IsNullOrEmpty(passwordHash); } public Task ChangePassword(User user, string newPassword) @@ -144,7 +118,7 @@ namespace Emby.Server.Implementations.Library if(passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt)) { passwordHash.SaltBytes = _cryptographyProvider.GenerateSalt(); - passwordHash.Salt = BitConverter.ToString(passwordHash.SaltBytes).Replace("-",""); + passwordHash.Salt = PasswordHash.ConvertToByteString(passwordHash.SaltBytes); passwordHash.Id = _cryptographyProvider.DefaultHashMethod; passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash); }else if (newPassword != null) @@ -164,19 +138,18 @@ namespace Emby.Server.Implementations.Library public string GetPasswordHash(User user) { - return string.IsNullOrEmpty(user.Password) - ? GetEmptyHashedString(user) - : user.Password; + return user.Password; } public string GetEmptyHashedString(User user) { - return GetHashedString(user, string.Empty); + return null; } - public string GetHashedStringChangeAuth(string NewPassword, PasswordHash passwordHash) + public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash) { - return BitConverter.ToString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(NewPassword), passwordHash.SaltBytes)).Replace("-", string.Empty); + passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword); + return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash)); } /// @@ -184,8 +157,6 @@ namespace Emby.Server.Implementations.Library /// public string GetHashedString(User user, string str) { - //This is legacy. Deprecated in the auth method. - //return BitConverter.ToString(_cryptoProvider2.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty); PasswordHash passwordHash; if (String.IsNullOrEmpty(user.Password)) { @@ -197,13 +168,15 @@ namespace Emby.Server.Implementations.Library passwordHash = new PasswordHash(user.Password); } if (passwordHash.SaltBytes != null) - { - return BitConverter.ToString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str), passwordHash.SaltBytes)).Replace("-",string.Empty); + { + //the password is modern format with PBKDF and we should take advantage of that + passwordHash.HashBytes = Encoding.UTF8.GetBytes(str); + return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash)); } else - { - return BitConverter.ToString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty); - //throw new Exception("User does not have a hash, this should not be possible"); + { + //the password has no salt and should be called with the older method for safety + return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str))); } diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index a139c4e73..b8777a480 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -217,9 +217,8 @@ namespace Emby.Server.Implementations.Library } } - public bool IsValidUsername(string username) + public static bool IsValidUsername(string username) { - //The old way was dumb, we should make it less dumb, lets do so. //This is some regex that matches only on unicode "word" characters, as well as -, _ and @ //In theory this will cut out most if not all 'control' characters which should help minimize any weirdness string UserNameRegex = "^[\\w-'._@]*$"; @@ -229,8 +228,7 @@ namespace Emby.Server.Implementations.Library private static bool IsValidUsernameCharacter(char i) { - string UserNameRegex = "^[\\w-'._@]*$"; - return Regex.IsMatch(i.ToString(), UserNameRegex); + return IsValidUsername(i.ToString()); } public string MakeValidUsername(string username) diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs index 524484b10..cd61657c1 100644 --- a/MediaBrowser.Model/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs @@ -16,70 +16,78 @@ namespace MediaBrowser.Model.Cryptography public byte[] SaltBytes; public string Hash; public byte[] HashBytes; - public PasswordHash(string StorageString) + public PasswordHash(string storageString) { - string[] a = StorageString.Split('$'); - Id = a[1]; - if (a[2].Contains("=")) + string[] SplitStorageString = storageString.Split('$'); + Id = SplitStorageString[1]; + if (SplitStorageString[2].Contains("=")) { - foreach (string paramset in (a[2].Split(','))) + foreach (string paramset in (SplitStorageString[2].Split(','))) { if (!String.IsNullOrEmpty(paramset)) { - string[] fields = paramset.Split('='); - Parameters.Add(fields[0], fields[1]); + string[] fields = paramset.Split('='); + if(fields.Length == 2) + { + Parameters.Add(fields[0], fields[1]); + } } } - if (a.Length == 4) + if (SplitStorageString.Length == 5) { - Salt = a[2]; - SaltBytes = FromByteString(Salt); - Hash = a[3]; - HashBytes = FromByteString(Hash); + Salt = SplitStorageString[3]; + SaltBytes = ConvertFromByteString(Salt); + Hash = SplitStorageString[4]; + HashBytes = ConvertFromByteString(Hash); } else { Salt = string.Empty; - Hash = a[3]; - HashBytes = FromByteString(Hash); + Hash = SplitStorageString[3]; + HashBytes = ConvertFromByteString(Hash); } } else { - if (a.Length == 4) + if (SplitStorageString.Length == 4) { - Salt = a[2]; - SaltBytes = FromByteString(Salt); - Hash = a[3]; - HashBytes = FromByteString(Hash); + Salt = SplitStorageString[2]; + SaltBytes = ConvertFromByteString(Salt); + Hash = SplitStorageString[3]; + HashBytes = ConvertFromByteString(Hash); } else { Salt = string.Empty; - Hash = a[2]; - HashBytes = FromByteString(Hash); + Hash = SplitStorageString[2]; + HashBytes = ConvertFromByteString(Hash); } } } - public PasswordHash(ICryptoProvider cryptoProvider2) + public PasswordHash(ICryptoProvider cryptoProvider) { - Id = "SHA256"; - SaltBytes = cryptoProvider2.GenerateSalt(); - Salt = BitConverter.ToString(SaltBytes).Replace("-", ""); + Id = cryptoProvider.DefaultHashMethod; + SaltBytes = cryptoProvider.GenerateSalt(); + Salt = ConvertToByteString(SaltBytes); } - private byte[] FromByteString(string ByteString) + public static byte[] ConvertFromByteString(string byteString) { List Bytes = new List(); - for (int i = 0; i < ByteString.Length; i += 2) + for (int i = 0; i < byteString.Length; i += 2) { - Bytes.Add(Convert.ToByte(ByteString.Substring(i, 2),16)); + Bytes.Add(Convert.ToByte(byteString.Substring(i, 2),16)); } return Bytes.ToArray(); - } + } + public static string ConvertToByteString(byte[] bytes) + { + return BitConverter.ToString(bytes).Replace("-", ""); + } + private string SerializeParameters() { string ReturnString = String.Empty; @@ -98,10 +106,15 @@ namespace MediaBrowser.Model.Cryptography { string OutString = "$"; OutString += Id; - if (!string.IsNullOrEmpty(SerializeParameters())) - OutString += $"${SerializeParameters()}"; + string paramstring = SerializeParameters(); + if (!string.IsNullOrEmpty(paramstring)) + { + OutString += $"${paramstring}"; + } if (!string.IsNullOrEmpty(Salt)) + { OutString += $"${Salt}"; + } OutString += $"${Hash}"; return OutString; } -- cgit v1.2.3 From 48e7274d3783e57f89f6e1cc76fcd8696e987ec5 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Mon, 18 Feb 2019 01:26:01 -0800 Subject: added justaman notes, fixed new bug from emty has removals --- .../Cryptography/CryptographyProvider.cs | 5 +-- .../Library/DefaultAuthenticationProvider.cs | 42 ++++++++++++++++------ Emby.Server.Implementations/Library/UserManager.cs | 4 +-- MediaBrowser.Model/Cryptography/PasswordHash.cs | 42 ++++++++++++---------- 4 files changed, 59 insertions(+), 34 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 436443f06..c4f034631 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Security.Cryptography; using System.Text; @@ -102,12 +103,12 @@ namespace Emby.Server.Implementations.Cryptography } else { - return PBKDF2(HashMethod, bytes, salt,defaultiterations); + return PBKDF2(HashMethod, bytes, salt, defaultiterations); } } else { - throw new CryptographicException($"Requested hash method is not supported: {HashMethod}")); + throw new CryptographicException($"Requested hash method is not supported: {HashMethod}"); } } diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 016de6db7..80026d97c 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -37,7 +37,17 @@ namespace Emby.Server.Implementations.Library if (resolvedUser == null) { throw new Exception("Invalid username or password"); - } + } + + //As long as jellyfin supports passwordless users, we need this little block here to accomodate + if (IsPasswordEmpty(resolvedUser, password)) + { + return Task.FromResult(new ProviderAuthenticationResult + { + Username = username + }); + } + ConvertPasswordFormat(resolvedUser); byte[] passwordbytes = Encoding.UTF8.GetBytes(password); @@ -106,15 +116,30 @@ namespace Emby.Server.Implementations.Library return Task.FromResult(hasConfiguredPassword); } - private bool IsPasswordEmpty(User user, string passwordHash) - { - return string.IsNullOrEmpty(passwordHash); + private bool IsPasswordEmpty(User user, string password) + { + if (string.IsNullOrEmpty(user.Password)) + { + return string.IsNullOrEmpty(password); + } + return false; } public Task ChangePassword(User user, string newPassword) { - //string newPasswordHash = null; - ConvertPasswordFormat(user); + ConvertPasswordFormat(user); + //This is needed to support changing a no password user to a password user + if (string.IsNullOrEmpty(user.Password)) + { + PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider); + newPasswordHash.SaltBytes = _cryptographyProvider.GenerateSalt(); + newPasswordHash.Salt = PasswordHash.ConvertToByteString(newPasswordHash.SaltBytes); + newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod; + newPasswordHash.Hash = GetHashedStringChangeAuth(newPassword, newPasswordHash); + user.Password = newPasswordHash.ToString(); + return Task.CompletedTask; + } + PasswordHash passwordHash = new PasswordHash(user.Password); if (passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt)) { @@ -143,11 +168,6 @@ namespace Emby.Server.Implementations.Library return user.Password; } - public string GetEmptyHashedString(User user) - { - return null; - } - public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash) { passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword); diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index b8777a480..3daed0c08 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -475,13 +475,13 @@ namespace Emby.Server.Implementations.Library private string GetLocalPasswordHash(User user) { return string.IsNullOrEmpty(user.EasyPassword) - ? _defaultAuthenticationProvider.GetEmptyHashedString(user) + ? null : user.EasyPassword; } private bool IsPasswordEmpty(User user, string passwordHash) { - return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase); + return string.IsNullOrEmpty(passwordHash); } /// diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs index cd61657c1..3a817543b 100644 --- a/MediaBrowser.Model/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs @@ -18,48 +18,52 @@ namespace MediaBrowser.Model.Cryptography public byte[] HashBytes; public PasswordHash(string storageString) { - string[] SplitStorageString = storageString.Split('$'); - Id = SplitStorageString[1]; - if (SplitStorageString[2].Contains("=")) + string[] splitted = storageString.Split('$'); + Id = splitted[1]; + if (splitted[2].Contains("=")) { - foreach (string paramset in (SplitStorageString[2].Split(','))) + foreach (string paramset in (splitted[2].Split(','))) { if (!String.IsNullOrEmpty(paramset)) { string[] fields = paramset.Split('='); - if(fields.Length == 2) + if (fields.Length == 2) { Parameters.Add(fields[0], fields[1]); + } + else + { + throw new Exception($"Malformed parameter in password hash string {paramset}"); } } } - if (SplitStorageString.Length == 5) + if (splitted.Length == 5) { - Salt = SplitStorageString[3]; + Salt = splitted[3]; SaltBytes = ConvertFromByteString(Salt); - Hash = SplitStorageString[4]; + Hash = splitted[4]; HashBytes = ConvertFromByteString(Hash); } else { Salt = string.Empty; - Hash = SplitStorageString[3]; + Hash = splitted[3]; HashBytes = ConvertFromByteString(Hash); } } else { - if (SplitStorageString.Length == 4) + if (splitted.Length == 4) { - Salt = SplitStorageString[2]; + Salt = splitted[2]; SaltBytes = ConvertFromByteString(Salt); - Hash = SplitStorageString[3]; + Hash = splitted[3]; HashBytes = ConvertFromByteString(Hash); } else { Salt = string.Empty; - Hash = SplitStorageString[2]; + Hash = splitted[2]; HashBytes = ConvertFromByteString(Hash); } @@ -83,6 +87,7 @@ namespace MediaBrowser.Model.Cryptography } return Bytes.ToArray(); } + public static string ConvertToByteString(byte[] bytes) { return BitConverter.ToString(bytes).Replace("-", ""); @@ -104,19 +109,18 @@ namespace MediaBrowser.Model.Cryptography public override string ToString() { - string OutString = "$"; - OutString += Id; + string outString = "$" +Id; string paramstring = SerializeParameters(); if (!string.IsNullOrEmpty(paramstring)) { - OutString += $"${paramstring}"; + outString += $"${paramstring}"; } if (!string.IsNullOrEmpty(Salt)) { - OutString += $"${Salt}"; + outString += $"${Salt}"; } - OutString += $"${Hash}"; - return OutString; + outString += $"${Hash}"; + return outString; } } -- cgit v1.2.3 From 6bbb968b578fe42224227b70e78825bbed5cfc6f Mon Sep 17 00:00:00 2001 From: Phallacy Date: Wed, 20 Feb 2019 00:00:26 -0800 Subject: minor changes and return to netstandard --- .../Cryptography/CryptographyProvider.cs | 5 +- .../Data/SqliteUserRepository.cs | 3 +- .../Emby.Server.Implementations.csproj | 2 +- .../Library/DefaultAuthenticationProvider.cs | 33 +++++---- Emby.Server.Implementations/Library/UserManager.cs | 3 +- MediaBrowser.Model/Cryptography/PasswordHash.cs | 81 ++++++++++++---------- 6 files changed, 72 insertions(+), 55 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index dc528c280..2f2fd9592 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -73,8 +73,9 @@ namespace Emby.Server.Implementations.Cryptography } private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations) - { - using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations, new HashAlgorithmName(method))) + { + //downgrading for now as we need this library to be dotnetstandard compliant + using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations)) { return r.GetBytes(32); } diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs index 1b6deae7d..3df91f71c 100644 --- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs @@ -54,7 +54,8 @@ namespace Emby.Server.Implementations.Data if (!localUsersTableExists && TableExists(connection, "Users")) { TryMigrateToLocalUsersTable(connection); - } + } + RemoveEmptyPasswordHashes(); } } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 86b2efe54..8356a9501 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -34,7 +34,7 @@ - netcoreapp2.1 + netstandard2.0 false diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 80026d97c..2ac3ef424 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -56,7 +56,7 @@ namespace Emby.Server.Implementations.Library string CalculatedHashString; if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id)) { - if (String.IsNullOrEmpty(readyHash.Salt)) + if (string.IsNullOrEmpty(readyHash.Salt)) { CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes); CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); @@ -65,7 +65,8 @@ namespace Emby.Server.Implementations.Library { CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes); CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); - } + } + if (CalculatedHashString == readyHash.Hash) { success = true; @@ -95,18 +96,20 @@ namespace Emby.Server.Implementations.Library private void ConvertPasswordFormat(User user) { if (!string.IsNullOrEmpty(user.Password)) + { + return; + } + + if (!user.Password.Contains("$")) { - if (!user.Password.Contains("$")) - { - string hash = user.Password; - user.Password = String.Format("$SHA1${0}", hash); - } - - if (user.EasyPassword != null && !user.EasyPassword.Contains("$")) - { - string hash = user.EasyPassword; - user.EasyPassword = string.Format("$SHA1${0}", hash); - } + string hash = user.Password; + user.Password = String.Format("$SHA1${0}", hash); + } + + if (user.EasyPassword != null && !user.EasyPassword.Contains("$")) + { + string hash = user.EasyPassword; + user.EasyPassword = string.Format("$SHA1${0}", hash); } } @@ -122,6 +125,7 @@ namespace Emby.Server.Implementations.Library { return string.IsNullOrEmpty(password); } + return false; } @@ -188,7 +192,8 @@ namespace Emby.Server.Implementations.Library { ConvertPasswordFormat(user); passwordHash = new PasswordHash(user.Password); - } + } + if (passwordHash.SaltBytes != null) { //the password is modern format with PBKDF and we should take advantage of that diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 3daed0c08..b74006233 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -221,9 +221,8 @@ namespace Emby.Server.Implementations.Library { //This is some regex that matches only on unicode "word" characters, as well as -, _ and @ //In theory this will cut out most if not all 'control' characters which should help minimize any weirdness - string UserNameRegex = "^[\\w-'._@]*$"; // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) - return Regex.IsMatch(username, UserNameRegex); + return Regex.IsMatch(username, "^[\\w-'._@]*$"); } private static bool IsValidUsernameCharacter(char i) diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs index 3a817543b..49bd510e9 100644 --- a/MediaBrowser.Model/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs @@ -10,26 +10,33 @@ namespace MediaBrowser.Model.Cryptography //https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md //$[$=(,=)*][$[$]] - public string Id; - public Dictionary Parameters = new Dictionary(); - public string Salt; - public byte[] SaltBytes; - public string Hash; - public byte[] HashBytes; + private string id; + private Dictionary parameters = new Dictionary(); + private string salt; + private byte[] saltBytes; + private string hash; + private byte[] hashBytes; + public string Id { get => id; set => id = value; } + public Dictionary Parameters { get => parameters; set => parameters = value; } + public string Salt { get => salt; set => salt = value; } + public byte[] SaltBytes { get => saltBytes; set => saltBytes = value; } + public string Hash { get => hash; set => hash = value; } + public byte[] HashBytes { get => hashBytes; set => hashBytes = value; } + public PasswordHash(string storageString) { string[] splitted = storageString.Split('$'); - Id = splitted[1]; + id = splitted[1]; if (splitted[2].Contains("=")) { foreach (string paramset in (splitted[2].Split(','))) { - if (!String.IsNullOrEmpty(paramset)) + if (!string.IsNullOrEmpty(paramset)) { string[] fields = paramset.Split('='); if (fields.Length == 2) { - Parameters.Add(fields[0], fields[1]); + parameters.Add(fields[0], fields[1]); } else { @@ -39,32 +46,32 @@ namespace MediaBrowser.Model.Cryptography } if (splitted.Length == 5) { - Salt = splitted[3]; - SaltBytes = ConvertFromByteString(Salt); - Hash = splitted[4]; - HashBytes = ConvertFromByteString(Hash); + salt = splitted[3]; + saltBytes = ConvertFromByteString(salt); + hash = splitted[4]; + hashBytes = ConvertFromByteString(hash); } else { - Salt = string.Empty; - Hash = splitted[3]; - HashBytes = ConvertFromByteString(Hash); + salt = string.Empty; + hash = splitted[3]; + hashBytes = ConvertFromByteString(hash); } } else { if (splitted.Length == 4) { - Salt = splitted[2]; - SaltBytes = ConvertFromByteString(Salt); - Hash = splitted[3]; - HashBytes = ConvertFromByteString(Hash); + salt = splitted[2]; + saltBytes = ConvertFromByteString(salt); + hash = splitted[3]; + hashBytes = ConvertFromByteString(hash); } else { - Salt = string.Empty; - Hash = splitted[2]; - HashBytes = ConvertFromByteString(Hash); + salt = string.Empty; + hash = splitted[2]; + hashBytes = ConvertFromByteString(hash); } } @@ -73,9 +80,9 @@ namespace MediaBrowser.Model.Cryptography public PasswordHash(ICryptoProvider cryptoProvider) { - Id = cryptoProvider.DefaultHashMethod; - SaltBytes = cryptoProvider.GenerateSalt(); - Salt = ConvertToByteString(SaltBytes); + id = cryptoProvider.DefaultHashMethod; + saltBytes = cryptoProvider.GenerateSalt(); + salt = ConvertToByteString(SaltBytes); } public static byte[] ConvertFromByteString(string byteString) @@ -95,31 +102,35 @@ namespace MediaBrowser.Model.Cryptography private string SerializeParameters() { - string ReturnString = String.Empty; - foreach (var KVP in Parameters) + string ReturnString = string.Empty; + foreach (var KVP in parameters) { - ReturnString += String.Format(",{0}={1}", KVP.Key, KVP.Value); - } + ReturnString += $",{KVP.Key}={KVP.Value}"; + } + if ((!string.IsNullOrEmpty(ReturnString)) && ReturnString[0] == ',') { ReturnString = ReturnString.Remove(0, 1); - } + } + return ReturnString; } public override string ToString() { - string outString = "$" +Id; + string outString = "$" +id; string paramstring = SerializeParameters(); if (!string.IsNullOrEmpty(paramstring)) { outString += $"${paramstring}"; } - if (!string.IsNullOrEmpty(Salt)) + + if (!string.IsNullOrEmpty(salt)) { - outString += $"${Salt}"; + outString += $"${salt}"; } - outString += $"${Hash}"; + + outString += $"${hash}"; return outString; } } -- cgit v1.2.3 From 2c26517172ca2c2f1df1c83d9300ad7c66667866 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Mon, 4 Mar 2019 23:58:25 -0800 Subject: minor style fixes --- .../Cryptography/CryptographyProvider.cs | 42 +++++----- .../Library/DefaultAuthenticationProvider.cs | 39 ++++----- Emby.Server.Implementations/Library/UserManager.cs | 2 +- MediaBrowser.Model/Cryptography/ICryptoProvider.cs | 42 +++++----- MediaBrowser.Model/Cryptography/PasswordHash.cs | 92 +++++++++++++--------- 5 files changed, 115 insertions(+), 102 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 3c9403ba8..cf1ea6efa 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -11,17 +11,21 @@ namespace Emby.Server.Implementations.Cryptography { public class CryptographyProvider : ICryptoProvider { - private HashSet SupportedHashMethods; + private HashSet _supportedHashMethods; + public string DefaultHashMethod => "PBKDF2"; - private RandomNumberGenerator rng; - private int defaultiterations = 1000; + + private RandomNumberGenerator _randomNumberGenerator; + + private int _defaultIterations = 1000; + public CryptographyProvider() { //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 - SupportedHashMethods = new HashSet() + _supportedHashMethods = new HashSet() { "MD5" ,"System.Security.Cryptography.MD5" @@ -38,7 +42,7 @@ namespace Emby.Server.Implementations.Cryptography ,"SHA-512" ,"System.Security.Cryptography.SHA512" }; - rng = RandomNumberGenerator.Create(); + _randomNumberGenerator = RandomNumberGenerator.Create(); } public Guid GetMD5(string str) @@ -72,7 +76,7 @@ namespace Emby.Server.Implementations.Cryptography public IEnumerable GetSupportedHashMethods() { - return SupportedHashMethods; + return _supportedHashMethods; } private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations) @@ -86,12 +90,13 @@ namespace Emby.Server.Implementations.Cryptography return r.GetBytes(32); } } + throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}"); } - public byte[] ComputeHash(string HashMethod, byte[] bytes) + public byte[] ComputeHash(string hashMethod, byte[] bytes) { - return ComputeHash(HashMethod, bytes, new byte[0]); + return ComputeHash(hashMethod, bytes, new byte[0]); } public byte[] ComputeHashWithDefaultMethod(byte[] bytes) @@ -99,15 +104,15 @@ namespace Emby.Server.Implementations.Cryptography return ComputeHash(DefaultHashMethod, bytes); } - public byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt) + public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt) { - if(HashMethod == DefaultHashMethod) + if(hashMethod == DefaultHashMethod) { - return PBKDF2(HashMethod, bytes, salt, defaultiterations); + return PBKDF2(hashMethod, bytes, salt, _defaultIterations); } - else if (SupportedHashMethods.Contains(HashMethod)) + else if (_supportedHashMethods.Contains(hashMethod)) { - using (var h = HashAlgorithm.Create(HashMethod)) + using (var h = HashAlgorithm.Create(hashMethod)) { if (salt.Length == 0) { @@ -121,21 +126,21 @@ namespace Emby.Server.Implementations.Cryptography } else { - throw new CryptographicException($"Requested hash method is not supported: {HashMethod}"); + throw new CryptographicException($"Requested hash method is not supported: {hashMethod}"); } } public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt) { - return PBKDF2(DefaultHashMethod, bytes, salt, defaultiterations); + return PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations); } public byte[] ComputeHash(PasswordHash hash) { - int iterations = defaultiterations; + int iterations = _defaultIterations; if (!hash.Parameters.ContainsKey("iterations")) { - hash.Parameters.Add("iterations", defaultiterations.ToString(CultureInfo.InvariantCulture)); + hash.Parameters.Add("iterations", _defaultIterations.ToString(CultureInfo.InvariantCulture)); } else { @@ -148,13 +153,14 @@ namespace Emby.Server.Implementations.Cryptography throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}", e); } } + return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes, iterations); } public byte[] GenerateSalt() { byte[] salt = new byte[64]; - rng.GetBytes(salt); + _randomNumberGenerator.GetBytes(salt); return salt; } } diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 7ccdccc0a..8f10b5a84 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -19,18 +19,16 @@ namespace Emby.Server.Implementations.Library public string Name => "Default"; public bool IsEnabled => true; - - - //This is dumb and an artifact of the backwards way auth providers were designed. - //This version of authenticate was never meant to be called, but needs to be here for interface compat - //Only the providers that don't provide local user support use this + + // This is dumb and an artifact of the backwards way auth providers were designed. + // This version of authenticate was never meant to be called, but needs to be here for interface compat + // Only the providers that don't provide local user support use this public Task Authenticate(string username, string password) { throw new NotImplementedException(); } - - - //This is the verson that we need to use for local users. Because reasons. + + // This is the verson that we need to use for local users. Because reasons. public Task Authenticate(string username, string password, User resolvedUser) { bool success = false; @@ -39,7 +37,7 @@ namespace Emby.Server.Implementations.Library throw new Exception("Invalid username or password"); } - //As long as jellyfin supports passwordless users, we need this little block here to accomodate + // As long as jellyfin supports passwordless users, we need this little block here to accomodate if (IsPasswordEmpty(resolvedUser, password)) { return Task.FromResult(new ProviderAuthenticationResult @@ -70,7 +68,7 @@ namespace Emby.Server.Implementations.Library if (CalculatedHashString == readyHash.Hash) { success = true; - //throw new Exception("Invalid username or password"); + // throw new Exception("Invalid username or password"); } } else @@ -78,7 +76,7 @@ namespace Emby.Server.Implementations.Library throw new Exception(String.Format($"Requested crypto method not available in provider: {readyHash.Id}")); } - //var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); + // var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); if (!success) { @@ -91,8 +89,8 @@ namespace Emby.Server.Implementations.Library }); } - //This allows us to move passwords forward to the newformat without breaking. They are still insecure, unsalted, and dumb before a password change - //but at least they are in the new format. + // This allows us to move passwords forward to the newformat without breaking. They are still insecure, unsalted, and dumb before a password change + // but at least they are in the new format. private void ConvertPasswordFormat(User user) { if (string.IsNullOrEmpty(user.Password)) @@ -121,18 +119,13 @@ namespace Emby.Server.Implementations.Library private bool IsPasswordEmpty(User user, string password) { - if (string.IsNullOrEmpty(user.Password)) - { - return string.IsNullOrEmpty(password); - } - - return false; + return (string.IsNullOrEmpty(user.Password) && string.IsNullOrEmpty(password)); } public Task ChangePassword(User user, string newPassword) { ConvertPasswordFormat(user); - //This is needed to support changing a no password user to a password user + // This is needed to support changing a no password user to a password user if (string.IsNullOrEmpty(user.Password)) { PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider); @@ -184,7 +177,7 @@ namespace Emby.Server.Implementations.Library public string GetHashedString(User user, string str) { PasswordHash passwordHash; - if (String.IsNullOrEmpty(user.Password)) + if (string.IsNullOrEmpty(user.Password)) { passwordHash = new PasswordHash(_cryptographyProvider); } @@ -196,13 +189,13 @@ namespace Emby.Server.Implementations.Library if (passwordHash.SaltBytes != null) { - //the password is modern format with PBKDF and we should take advantage of that + // the password is modern format with PBKDF and we should take advantage of that passwordHash.HashBytes = Encoding.UTF8.GetBytes(str); return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash)); } else { - //the password has no salt and should be called with the older method for safety + // the password has no salt and should be called with the older method for safety return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str))); } } diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 0f188ca75..57bf16364 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -75,7 +75,7 @@ namespace Emby.Server.Implementations.Library private readonly Func _dtoServiceFactory; private readonly IServerApplicationHost _appHost; private readonly IFileSystem _fileSystem; - + private IAuthenticationProvider[] _authenticationProviders; private DefaultAuthenticationProvider _defaultAuthenticationProvider; diff --git a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs index 8accc696e..5988112c2 100644 --- a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs +++ b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs @@ -1,22 +1,22 @@ -using System; -using System.IO; -using System.Collections.Generic; - -namespace MediaBrowser.Model.Cryptography -{ - public interface ICryptoProvider - { - Guid GetMD5(string str); - byte[] ComputeMD5(Stream str); - byte[] ComputeMD5(byte[] bytes); - byte[] ComputeSHA1(byte[] bytes); - IEnumerable GetSupportedHashMethods(); - byte[] ComputeHash(string HashMethod, byte[] bytes); - byte[] ComputeHashWithDefaultMethod(byte[] bytes); - byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt); - byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt); - byte[] ComputeHash(PasswordHash hash); +using System; +using System.IO; +using System.Collections.Generic; + +namespace MediaBrowser.Model.Cryptography +{ + public interface ICryptoProvider + { + Guid GetMD5(string str); + byte[] ComputeMD5(Stream str); + byte[] ComputeMD5(byte[] bytes); + byte[] ComputeSHA1(byte[] bytes); + IEnumerable GetSupportedHashMethods(); + byte[] ComputeHash(string HashMethod, byte[] bytes); + byte[] ComputeHashWithDefaultMethod(byte[] bytes); + byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt); + byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt); + byte[] ComputeHash(PasswordHash hash); byte[] GenerateSalt(); - string DefaultHashMethod { get; } - } -} + string DefaultHashMethod { get; } + } +} diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs index 49bd510e9..a52840404 100644 --- a/MediaBrowser.Model/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs @@ -6,27 +6,40 @@ namespace MediaBrowser.Model.Cryptography { public class PasswordHash { - //Defined from this hash storage spec - //https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md - //$[$=(,=)*][$[$]] + // Defined from this hash storage spec + // https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md + // $[$=(,=)*][$[$]] + // with one slight amendment to ease the transition, we're writing out the bytes in hex + // rather than making them a BASE64 string with stripped padding - private string id; - private Dictionary parameters = new Dictionary(); - private string salt; - private byte[] saltBytes; - private string hash; - private byte[] hashBytes; - public string Id { get => id; set => id = value; } - public Dictionary Parameters { get => parameters; set => parameters = value; } - public string Salt { get => salt; set => salt = value; } - public byte[] SaltBytes { get => saltBytes; set => saltBytes = value; } - public string Hash { get => hash; set => hash = value; } - public byte[] HashBytes { get => hashBytes; set => hashBytes = value; } + private string _id; + + private Dictionary _parameters = new Dictionary(); + + private string _salt; + + private byte[] _saltBytes; + + private string _hash; + + private byte[] _hashBytes; + + public string Id { get => _id; set => _id = value; } + + public Dictionary Parameters { get => _parameters; set => _parameters = value; } + + public string Salt { get => _salt; set => _salt = value; } + + public byte[] SaltBytes { get => _saltBytes; set => _saltBytes = value; } + + public string Hash { get => _hash; set => _hash = value; } + + public byte[] HashBytes { get => _hashBytes; set => _hashBytes = value; } public PasswordHash(string storageString) { string[] splitted = storageString.Split('$'); - id = splitted[1]; + _id = splitted[1]; if (splitted[2].Contains("=")) { foreach (string paramset in (splitted[2].Split(','))) @@ -36,7 +49,7 @@ namespace MediaBrowser.Model.Cryptography string[] fields = paramset.Split('='); if (fields.Length == 2) { - parameters.Add(fields[0], fields[1]); + _parameters.Add(fields[0], fields[1]); } else { @@ -46,32 +59,32 @@ namespace MediaBrowser.Model.Cryptography } if (splitted.Length == 5) { - salt = splitted[3]; - saltBytes = ConvertFromByteString(salt); - hash = splitted[4]; - hashBytes = ConvertFromByteString(hash); + _salt = splitted[3]; + _saltBytes = ConvertFromByteString(_salt); + _hash = splitted[4]; + _hashBytes = ConvertFromByteString(_hash); } else { - salt = string.Empty; - hash = splitted[3]; - hashBytes = ConvertFromByteString(hash); + _salt = string.Empty; + _hash = splitted[3]; + _hashBytes = ConvertFromByteString(_hash); } } else { if (splitted.Length == 4) { - salt = splitted[2]; - saltBytes = ConvertFromByteString(salt); - hash = splitted[3]; - hashBytes = ConvertFromByteString(hash); + _salt = splitted[2]; + _saltBytes = ConvertFromByteString(_salt); + _hash = splitted[3]; + _hashBytes = ConvertFromByteString(_hash); } else { - salt = string.Empty; - hash = splitted[2]; - hashBytes = ConvertFromByteString(hash); + _salt = string.Empty; + _hash = splitted[2]; + _hashBytes = ConvertFromByteString(_hash); } } @@ -80,9 +93,9 @@ namespace MediaBrowser.Model.Cryptography public PasswordHash(ICryptoProvider cryptoProvider) { - id = cryptoProvider.DefaultHashMethod; - saltBytes = cryptoProvider.GenerateSalt(); - salt = ConvertToByteString(SaltBytes); + _id = cryptoProvider.DefaultHashMethod; + _saltBytes = cryptoProvider.GenerateSalt(); + _salt = ConvertToByteString(SaltBytes); } public static byte[] ConvertFromByteString(string byteString) @@ -92,6 +105,7 @@ namespace MediaBrowser.Model.Cryptography { Bytes.Add(Convert.ToByte(byteString.Substring(i, 2),16)); } + return Bytes.ToArray(); } @@ -103,7 +117,7 @@ namespace MediaBrowser.Model.Cryptography private string SerializeParameters() { string ReturnString = string.Empty; - foreach (var KVP in parameters) + foreach (var KVP in _parameters) { ReturnString += $",{KVP.Key}={KVP.Value}"; } @@ -118,19 +132,19 @@ namespace MediaBrowser.Model.Cryptography public override string ToString() { - string outString = "$" +id; + string outString = "$" +_id; string paramstring = SerializeParameters(); if (!string.IsNullOrEmpty(paramstring)) { outString += $"${paramstring}"; } - if (!string.IsNullOrEmpty(salt)) + if (!string.IsNullOrEmpty(_salt)) { - outString += $"${salt}"; + outString += $"${_salt}"; } - outString += $"${hash}"; + outString += $"${_hash}"; return outString; } } -- cgit v1.2.3 From bef665be364ce1477d09ed268f68c19e0099922f Mon Sep 17 00:00:00 2001 From: Phallacy Date: Tue, 5 Mar 2019 23:45:05 -0800 Subject: Minor fixes to address style issues --- .../Library/DefaultAuthenticationProvider.cs | 14 +++++++------- Emby.Server.Implementations/Library/UserManager.cs | 15 +++++---------- MediaBrowser.Model/Cryptography/PasswordHash.cs | 7 ++++--- 3 files changed, 16 insertions(+), 20 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 8f10b5a84..3ac604b40 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -50,22 +50,22 @@ namespace Emby.Server.Implementations.Library byte[] passwordbytes = Encoding.UTF8.GetBytes(password); PasswordHash readyHash = new PasswordHash(resolvedUser.Password); - byte[] CalculatedHash; - string CalculatedHashString; + byte[] calculatedHash; + string calculatedHashString; if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id)) { if (string.IsNullOrEmpty(readyHash.Salt)) { - CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes); - CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); + calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes); + calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty); } else { - CalculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes); - CalculatedHashString = BitConverter.ToString(CalculatedHash).Replace("-", string.Empty); + calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes); + calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty); } - if (CalculatedHashString == readyHash.Hash) + if (calculatedHashString == readyHash.Hash) { success = true; // throw new Exception("Invalid username or password"); diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 57bf16364..efb1ef4a5 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -475,11 +475,6 @@ namespace Emby.Server.Implementations.Library : user.EasyPassword; } - private bool IsPasswordEmpty(User user, string passwordHash) - { - return string.IsNullOrEmpty(passwordHash); - } - /// /// Loads the users from the repository /// @@ -522,14 +517,14 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(user)); } - var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; - var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user)); + bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; + bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user)); - var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? + bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? hasConfiguredEasyPassword : hasConfiguredPassword; - var dto = new UserDto + UserDto dto = new UserDto { Id = user.Id, Name = user.Name, @@ -548,7 +543,7 @@ namespace Emby.Server.Implementations.Library dto.EnableAutoLogin = true; } - var image = user.GetImageInfo(ImageType.Primary, 0); + ItemImageInfo image = user.GetImageInfo(ImageType.Primary, 0); if (image != null) { diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs index a52840404..7a1be833d 100644 --- a/MediaBrowser.Model/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs @@ -100,13 +100,14 @@ namespace MediaBrowser.Model.Cryptography public static byte[] ConvertFromByteString(string byteString) { - List Bytes = new List(); + List bytes = new List(); for (int i = 0; i < byteString.Length; i += 2) { - Bytes.Add(Convert.ToByte(byteString.Substring(i, 2),16)); + // TODO: NetStandard2.1 switch this to use a span instead of a substring. + bytes.Add(Convert.ToByte(byteString.Substring(i, 2),16)); } - return Bytes.ToArray(); + return bytes.ToArray(); } public static string ConvertToByteString(byte[] bytes) -- cgit v1.2.3 From 1ee016c99745ed4a29f8995de1478ab6a6e410e9 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Sat, 16 Mar 2019 00:18:52 -0700 Subject: configurable user lockout --- Emby.Server.Implementations/Library/UserManager.cs | 14 +++++++++++--- MediaBrowser.Model/Users/UserPolicy.cs | 3 +++ 2 files changed, 14 insertions(+), 3 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index efb1ef4a5..e20af003d 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -219,7 +219,7 @@ namespace Emby.Server.Implementations.Library //This is some regex that matches only on unicode "word" characters, as well as -, _ and @ //In theory this will cut out most if not all 'control' characters which should help minimize any weirdness // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) - return Regex.IsMatch(username, "^[\\w-'._@]*$"); + return Regex.IsMatch(username, @"^[\w-'._@]*$"); } private static bool IsValidUsernameCharacter(char i) @@ -448,11 +448,19 @@ namespace Emby.Server.Implementations.Library user.Policy.InvalidLoginAttemptCount = newValue; - var maxCount = user.Policy.IsAdministrator ? 3 : 5; + // Check for users without a value here and then fill in the default value + // also protect from an always lockout if misconfigured + if (user.Policy.LoginAttemptsBeforeLockout == null || user.Policy.LoginAttemptsBeforeLockout == 0) + { + user.Policy.LoginAttemptsBeforeLockout = user.Policy.IsAdministrator ? 5 : 3; + } + + var maxCount = user.Policy.LoginAttemptsBeforeLockout; var fireLockout = false; - if (newValue >= maxCount) + // -1 can be used to specify no lockout value + if (maxCount != -1 && newValue >= maxCount) { _logger.LogDebug("Disabling user {0} due to {1} unsuccessful login attempts.", user.Name, newValue); user.Policy.IsDisabled = true; diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 27ce23778..5415fd5e8 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -66,6 +66,7 @@ namespace MediaBrowser.Model.Users public bool EnableAllFolders { get; set; } public int InvalidLoginAttemptCount { get; set; } + public int? LoginAttemptsBeforeLockout { get; set; } public bool EnablePublicSharing { get; set; } @@ -104,6 +105,8 @@ namespace MediaBrowser.Model.Users AccessSchedules = Array.Empty(); + LoginAttemptsBeforeLockout = -1; + EnableAllChannels = true; EnabledChannels = Array.Empty(); -- cgit v1.2.3 From 7f0fa74467d5f3560addd6cfefbfb2dbd24f4060 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Sat, 16 Mar 2019 00:38:31 -0700 Subject: updated regex to string literal with escaped - --- Emby.Server.Implementations/Library/UserManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index efb1ef4a5..7e2419b68 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -216,10 +216,10 @@ namespace Emby.Server.Implementations.Library public static bool IsValidUsername(string username) { - //This is some regex that matches only on unicode "word" characters, as well as -, _ and @ - //In theory this will cut out most if not all 'control' characters which should help minimize any weirdness + // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ + // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) - return Regex.IsMatch(username, "^[\\w-'._@]*$"); + return Regex.IsMatch(username, @"^[\w\-'._@]*$"); } private static bool IsValidUsernameCharacter(char i) -- cgit v1.2.3 From e3dbed1c1aff986e51e25ba244a6a6bf52816dbe Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sat, 16 Mar 2019 10:16:23 -0700 Subject: Update Emby.Server.Implementations/Library/UserManager.cs Co-Authored-By: LogicalPhallacy <44458166+LogicalPhallacy@users.noreply.github.com> --- Emby.Server.Implementations/Library/UserManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 7e2419b68..ee90b3558 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -218,7 +218,7 @@ namespace Emby.Server.Implementations.Library { // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness - // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) + // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.) return Regex.IsMatch(username, @"^[\w\-'._@]*$"); } -- cgit v1.2.3 From 80aedcd7e24a7708f44491e3cd359f911a124c8f Mon Sep 17 00:00:00 2001 From: Phallacy Date: Sat, 16 Mar 2019 21:36:45 -0700 Subject: really fixed line endings --- .../Library/DefaultPasswordResetProvider.cs | 22 - Emby.Server.Implementations/Library/UserManager.cs | 2438 ++++++++++---------- 2 files changed, 1219 insertions(+), 1241 deletions(-) delete mode 100644 Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs deleted file mode 100644 index 87dbe684c..000000000 --- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Threading.Tasks; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Entities; - -namespace Emby.Server.Implementations.Library -{ - public class DefaultPasswordResetProvider : IPasswordResetProvider - { - public string Name => "Default Password Reset"; - - public bool IsEnabled => true; - - // set our default timeout to an hour since we'll be making the PIN it generates a little less fragile - public TimeSpan PasswordResetTimeout => new TimeSpan(1,0,0); - - public Task ResetPassword(User user) - { - throw new NotImplementedException(); - } - } -} diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index ce8c01660..4cf703add 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -1,1219 +1,1219 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Events; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Security; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Library -{ - /// - /// Class UserManager - /// - public class UserManager : IUserManager - { - /// - /// Gets the users. - /// - /// The users. - public IEnumerable Users => _users; - - private User[] _users; - - /// - /// The _logger - /// - private readonly ILogger _logger; - - /// - /// Gets or sets the configuration manager. - /// - /// The configuration manager. - private IServerConfigurationManager ConfigurationManager { get; set; } - - /// - /// Gets the active user repository - /// - /// The user repository. - private IUserRepository UserRepository { get; set; } - public event EventHandler> UserPasswordChanged; - - private readonly IXmlSerializer _xmlSerializer; - private readonly IJsonSerializer _jsonSerializer; - - private readonly INetworkManager _networkManager; - - private readonly Func _imageProcessorFactory; - private readonly Func _dtoServiceFactory; - private readonly IServerApplicationHost _appHost; - private readonly IFileSystem _fileSystem; - - private IAuthenticationProvider[] _authenticationProviders; - private DefaultAuthenticationProvider _defaultAuthenticationProvider; - - public UserManager( - ILoggerFactory loggerFactory, - IServerConfigurationManager configurationManager, - IUserRepository userRepository, - IXmlSerializer xmlSerializer, - INetworkManager networkManager, - Func imageProcessorFactory, - Func dtoServiceFactory, - IServerApplicationHost appHost, - IJsonSerializer jsonSerializer, - IFileSystem fileSystem) - { - _logger = loggerFactory.CreateLogger(nameof(UserManager)); - UserRepository = userRepository; - _xmlSerializer = xmlSerializer; - _networkManager = networkManager; - _imageProcessorFactory = imageProcessorFactory; - _dtoServiceFactory = dtoServiceFactory; - _appHost = appHost; - _jsonSerializer = jsonSerializer; - _fileSystem = fileSystem; - ConfigurationManager = configurationManager; - _users = Array.Empty(); - - DeletePinFile(); - } - - public NameIdPair[] GetAuthenticationProviders() - { - return _authenticationProviders - .Where(i => i.IsEnabled) - .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1) - .ThenBy(i => i.Name) - .Select(i => new NameIdPair - { - Name = i.Name, - Id = GetAuthenticationProviderId(i) - }) - .ToArray(); - } - - public void AddParts(IEnumerable authenticationProviders) - { - _authenticationProviders = authenticationProviders.ToArray(); - - _defaultAuthenticationProvider = _authenticationProviders.OfType().First(); - } - - #region UserUpdated Event - /// - /// Occurs when [user updated]. - /// - public event EventHandler> UserUpdated; - public event EventHandler> UserPolicyUpdated; - public event EventHandler> UserConfigurationUpdated; - public event EventHandler> UserLockedOut; - - /// - /// Called when [user updated]. - /// - /// The user. - private void OnUserUpdated(User user) - { - UserUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); - } - #endregion - - #region UserDeleted Event - /// - /// Occurs when [user deleted]. - /// - public event EventHandler> UserDeleted; - /// - /// Called when [user deleted]. - /// - /// The user. - private void OnUserDeleted(User user) - { - UserDeleted?.Invoke(this, new GenericEventArgs { Argument = user }); - } - #endregion - - /// - /// Gets a User by Id - /// - /// The id. - /// User. - /// - public User GetUserById(Guid id) - { - if (id == Guid.Empty) - { - throw new ArgumentException(nameof(id), "Guid can't be empty"); - } - - return Users.FirstOrDefault(u => u.Id == id); - } - - /// - /// Gets the user by identifier. - /// - /// The identifier. - /// User. - public User GetUserById(string id) - { - return GetUserById(new Guid(id)); - } - - public User GetUserByName(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase)); - } - - public void Initialize() - { - _users = LoadUsers(); - - var users = Users.ToList(); - - // If there are no local users with admin rights, make them all admins - if (!users.Any(i => i.Policy.IsAdministrator)) - { - foreach (var user in users) - { - user.Policy.IsAdministrator = true; - UpdateUserPolicy(user, user.Policy, false); - } - } - } - - public static bool IsValidUsername(string username) - { - // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ - // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness - // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.) - return Regex.IsMatch(username, @"^[\w\-'._@]*$"); - } - - private static bool IsValidUsernameCharacter(char i) - { - return IsValidUsername(i.ToString()); - } - - public string MakeValidUsername(string username) - { - if (IsValidUsername(username)) - { - return username; - } - - // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) - var builder = new StringBuilder(); - - foreach (var c in username) - { - if (IsValidUsernameCharacter(c)) - { - builder.Append(c); - } - } - return builder.ToString(); - } - - public async Task AuthenticateUser(string username, string password, string hashedPassword, string remoteEndPoint, bool isUserSession) - { - if (string.IsNullOrWhiteSpace(username)) - { - throw new ArgumentNullException(nameof(username)); - } - - var user = Users - .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); - - var success = false; - IAuthenticationProvider authenticationProvider = null; - - if (user != null) - { - var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false); - authenticationProvider = authResult.Item1; - success = authResult.Item2; - } - else - { - // user is null - var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); - authenticationProvider = authResult.Item1; - success = authResult.Item2; - - if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) - { - user = await CreateUser(username).ConfigureAwait(false); - - var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy; - if (hasNewUserPolicy != null) - { - var policy = hasNewUserPolicy.GetNewUserPolicy(); - UpdateUserPolicy(user, policy, true); - } - } - } - - if (success && user != null && authenticationProvider != null) - { - var providerId = GetAuthenticationProviderId(authenticationProvider); - - if (!string.Equals(providerId, user.Policy.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) - { - user.Policy.AuthenticationProviderId = providerId; - UpdateUserPolicy(user, user.Policy, true); - } - } - - if (user == null) - { - throw new SecurityException("Invalid username or password entered."); - } - - if (user.Policy.IsDisabled) - { - throw new SecurityException(string.Format("The {0} account is currently disabled. Please consult with your administrator.", user.Name)); - } - - if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint)) - { - throw new SecurityException("Forbidden."); - } - - if (!user.IsParentalScheduleAllowed()) - { - throw new SecurityException("User is not allowed access at this time."); - } - - // Update LastActivityDate and LastLoginDate, then save - if (success) - { - if (isUserSession) - { - user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; - UpdateUser(user); - } - UpdateInvalidLoginAttemptCount(user, 0); - } - else - { - UpdateInvalidLoginAttemptCount(user, user.Policy.InvalidLoginAttemptCount + 1); - } - - _logger.LogInformation("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied"); - - return success ? user : null; - } - - private static string GetAuthenticationProviderId(IAuthenticationProvider provider) - { - return provider.GetType().FullName; - } - - private IAuthenticationProvider GetAuthenticationProvider(User user) - { - return GetAuthenticationProviders(user).First(); - } - - private IAuthenticationProvider[] GetAuthenticationProviders(User user) - { - var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId; - - var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray(); - - if (!string.IsNullOrEmpty(authenticationProviderId)) - { - providers = providers.Where(i => string.Equals(authenticationProviderId, GetAuthenticationProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray(); - } - - if (providers.Length == 0) - { - providers = new IAuthenticationProvider[] { _defaultAuthenticationProvider }; - } - - return providers; - } - - private async Task AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) - { - try - { - var requiresResolvedUser = provider as IRequiresResolvedUser; - if (requiresResolvedUser != null) - { - await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); - } - else - { - await provider.Authenticate(username, password).ConfigureAwait(false); - } - - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name); - - return false; - } - } - - private async Task> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) - { - bool success = false; - IAuthenticationProvider authenticationProvider = null; - - if (password != null && user != null) - { - // Doesn't look like this is even possible to be used, because of password == null checks below - hashedPassword = _defaultAuthenticationProvider.GetHashedString(user, password); - } - - if (password == null) - { - // legacy - success = string.Equals(_defaultAuthenticationProvider.GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); - } - else - { - foreach (var provider in GetAuthenticationProviders(user)) - { - success = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); - - if (success) - { - authenticationProvider = provider; - break; - } - } - } - - if (user != null) - { - if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword) - { - if (password == null) - { - // legacy - success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); - } - else - { - success = string.Equals(GetLocalPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); - } - } - } - - return new Tuple(authenticationProvider, success); - } - - private void UpdateInvalidLoginAttemptCount(User user, int newValue) - { - if (user.Policy.InvalidLoginAttemptCount == newValue || newValue <= 0) - { - return; - } - - user.Policy.InvalidLoginAttemptCount = newValue; - - // Check for users without a value here and then fill in the default value - // also protect from an always lockout if misconfigured - if (user.Policy.LoginAttemptsBeforeLockout == null || user.Policy.LoginAttemptsBeforeLockout == 0) - { - user.Policy.LoginAttemptsBeforeLockout = user.Policy.IsAdministrator ? 5 : 3; - } - - var maxCount = user.Policy.LoginAttemptsBeforeLockout; - - var fireLockout = false; - - // -1 can be used to specify no lockout value - if (maxCount != -1 && newValue >= maxCount) - { - _logger.LogDebug("Disabling user {0} due to {1} unsuccessful login attempts.", user.Name, newValue); - user.Policy.IsDisabled = true; - - fireLockout = true; - } - - UpdateUserPolicy(user, user.Policy, false); - - if (fireLockout) - { - UserLockedOut?.Invoke(this, new GenericEventArgs(user)); - } - } - - private string GetLocalPasswordHash(User user) - { - return string.IsNullOrEmpty(user.EasyPassword) - ? null - : user.EasyPassword; - } - - /// - /// Loads the users from the repository - /// - /// IEnumerable{User}. - private User[] LoadUsers() - { - var users = UserRepository.RetrieveAllUsers(); - - // There always has to be at least one user. - if (users.Count == 0) - { - var defaultName = Environment.UserName; - if (string.IsNullOrWhiteSpace(defaultName)) - { - defaultName = "MyJellyfinUser"; - } - var name = MakeValidUsername(defaultName); - - var user = InstantiateNewUser(name); - - user.DateLastSaved = DateTime.UtcNow; - - UserRepository.CreateUser(user); - - users.Add(user); - - user.Policy.IsAdministrator = true; - user.Policy.EnableContentDeletion = true; - user.Policy.EnableRemoteControlOfOtherUsers = true; - UpdateUserPolicy(user, user.Policy, false); - } - - return users.ToArray(); - } - - public UserDto GetUserDto(User user, string remoteEndPoint = null) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; - bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user)); - - bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? - hasConfiguredEasyPassword : - hasConfiguredPassword; - - UserDto dto = new UserDto - { - Id = user.Id, - Name = user.Name, - HasPassword = hasPassword, - HasConfiguredPassword = hasConfiguredPassword, - HasConfiguredEasyPassword = hasConfiguredEasyPassword, - LastActivityDate = user.LastActivityDate, - LastLoginDate = user.LastLoginDate, - Configuration = user.Configuration, - ServerId = _appHost.SystemId, - Policy = user.Policy - }; - - if (!hasPassword && Users.Count() == 1) - { - dto.EnableAutoLogin = true; - } - - ItemImageInfo image = user.GetImageInfo(ImageType.Primary, 0); - - if (image != null) - { - dto.PrimaryImageTag = GetImageCacheTag(user, image); - - try - { - _dtoServiceFactory().AttachPrimaryImageAspectRatio(dto, user); - } - catch (Exception ex) - { - // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions - _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {user}", user.Name); - } - } - - return dto; - } - - public UserDto GetOfflineUserDto(User user) - { - var dto = GetUserDto(user); - - dto.ServerName = _appHost.FriendlyName; - - return dto; - } - - private string GetImageCacheTag(BaseItem item, ItemImageInfo image) - { - try - { - return _imageProcessorFactory().GetImageCacheTag(item, image); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting {imageType} image info for {imagePath}", image.Type, image.Path); - return null; - } - } - - /// - /// Refreshes metadata for each user - /// - /// The cancellation token. - /// Task. - public async Task RefreshUsersMetadata(CancellationToken cancellationToken) - { - foreach (var user in Users) - { - await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), cancellationToken).ConfigureAwait(false); - } - } - - /// - /// Renames the user. - /// - /// The user. - /// The new name. - /// Task. - /// user - /// - public async Task RenameUser(User user, string newName) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (string.IsNullOrEmpty(newName)) - { - throw new ArgumentNullException(nameof(newName)); - } - - if (Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) - { - throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName)); - } - - if (user.Name.Equals(newName, StringComparison.Ordinal)) - { - throw new ArgumentException("The new and old names must be different."); - } - - await user.Rename(newName); - - OnUserUpdated(user); - } - - /// - /// Updates the user. - /// - /// The user. - /// user - /// - public void UpdateUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (user.Id.Equals(Guid.Empty) || !Users.Any(u => u.Id.Equals(user.Id))) - { - throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id)); - } - - user.DateModified = DateTime.UtcNow; - user.DateLastSaved = DateTime.UtcNow; - - UserRepository.UpdateUser(user); - - OnUserUpdated(user); - } - - public event EventHandler> UserCreated; - - private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1); - - /// - /// Creates the user. - /// - /// The name. - /// User. - /// name - /// - public async Task CreateUser(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - if (!IsValidUsername(name)) - { - throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); - } - - if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) - { - throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name)); - } - - await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); - - try - { - var user = InstantiateNewUser(name); - - var list = Users.ToList(); - list.Add(user); - _users = list.ToArray(); - - user.DateLastSaved = DateTime.UtcNow; - - UserRepository.CreateUser(user); - - EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs { Argument = user }, _logger); - - return user; - } - finally - { - _userListLock.Release(); - } - } - - /// - /// Deletes the user. - /// - /// The user. - /// Task. - /// user - /// - public async Task DeleteUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - var allUsers = Users.ToList(); - - if (allUsers.FirstOrDefault(u => u.Id == user.Id) == null) - { - throw new ArgumentException(string.Format("The user cannot be deleted because there is no user with the Name {0} and Id {1}.", user.Name, user.Id)); - } - - if (allUsers.Count == 1) - { - throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one user in the system.", user.Name)); - } - - if (user.Policy.IsAdministrator && allUsers.Count(i => i.Policy.IsAdministrator) == 1) - { - throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one admin user in the system.", user.Name)); - } - - await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); - - try - { - var configPath = GetConfigurationFilePath(user); - - UserRepository.DeleteUser(user); - - try - { - _fileSystem.DeleteFile(configPath); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting file {path}", configPath); - } - - DeleteUserPolicy(user); - - _users = allUsers.Where(i => i.Id != user.Id).ToArray(); - - OnUserDeleted(user); - } - finally - { - _userListLock.Release(); - } - } - - /// - /// Resets the password by clearing it. - /// - /// Task. - public Task ResetPassword(User user) - { - return ChangePassword(user, string.Empty); - } - - public void ResetEasyPassword(User user) - { - ChangeEasyPassword(user, string.Empty, null); - } - - public async Task ChangePassword(User user, string newPassword) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); - - UpdateUser(user); - - UserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); - } - - public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (newPassword != null) - { - newPasswordHash = _defaultAuthenticationProvider.GetHashedString(user, newPassword); - } - - if (string.IsNullOrWhiteSpace(newPasswordHash)) - { - throw new ArgumentNullException(nameof(newPasswordHash)); - } - - user.EasyPassword = newPasswordHash; - - UpdateUser(user); - - UserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); - } - - /// - /// Instantiates the new user. - /// - /// The name. - /// User. - private static User InstantiateNewUser(string name) - { - return new User - { - Name = name, - Id = Guid.NewGuid(), - DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, - UsesIdForConfigurationPath = true, - //Salt = BCrypt.GenerateSalt() - }; - } - - private string PasswordResetFile => Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "passwordreset.txt"); - - private string _lastPin; - private PasswordPinCreationResult _lastPasswordPinCreationResult; - private int _pinAttempts; - - private async Task CreatePasswordResetPin() - { - var num = new Random().Next(1, 9999); - - var path = PasswordResetFile; - - var pin = num.ToString("0000", CultureInfo.InvariantCulture); - _lastPin = pin; - - var time = TimeSpan.FromMinutes(5); - var expiration = DateTime.UtcNow.Add(time); - - var text = new StringBuilder(); - - var localAddress = (await _appHost.GetLocalApiUrl(CancellationToken.None).ConfigureAwait(false)) ?? string.Empty; - - text.AppendLine("Use your web browser to visit:"); - text.AppendLine(string.Empty); - text.AppendLine(localAddress + "/web/index.html#!/forgotpasswordpin.html"); - text.AppendLine(string.Empty); - text.AppendLine("Enter the following pin code:"); - text.AppendLine(string.Empty); - text.AppendLine(pin); - text.AppendLine(string.Empty); - - var localExpirationTime = expiration.ToLocalTime(); - // Tuesday, 22 August 2006 06:30 AM - text.AppendLine("The pin code will expire at " + localExpirationTime.ToString("f1", CultureInfo.CurrentCulture)); - - File.WriteAllText(path, text.ToString(), Encoding.UTF8); - - var result = new PasswordPinCreationResult - { - PinFile = path, - ExpirationDate = expiration - }; - - _lastPasswordPinCreationResult = result; - _pinAttempts = 0; - - return result; - } - - public async Task StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) - { - DeletePinFile(); - - var user = string.IsNullOrWhiteSpace(enteredUsername) ? - null : - GetUserByName(enteredUsername); - - var action = ForgotPasswordAction.InNetworkRequired; - string pinFile = null; - DateTime? expirationDate = null; - - if (user != null && !user.Policy.IsAdministrator) - { - action = ForgotPasswordAction.ContactAdmin; - } - else - { - if (isInNetwork) - { - action = ForgotPasswordAction.PinCode; - } - - var result = await CreatePasswordResetPin().ConfigureAwait(false); - pinFile = result.PinFile; - expirationDate = result.ExpirationDate; - } - - return new ForgotPasswordResult - { - Action = action, - PinFile = pinFile, - PinExpirationDate = expirationDate - }; - } - - public async Task RedeemPasswordResetPin(string pin) - { - DeletePinFile(); - - var usersReset = new List(); - - var valid = !string.IsNullOrWhiteSpace(_lastPin) && - string.Equals(_lastPin, pin, StringComparison.OrdinalIgnoreCase) && - _lastPasswordPinCreationResult != null && - _lastPasswordPinCreationResult.ExpirationDate > DateTime.UtcNow; - - if (valid) - { - _lastPin = null; - _lastPasswordPinCreationResult = null; - - foreach (var user in Users) - { - await ResetPassword(user).ConfigureAwait(false); - - if (user.Policy.IsDisabled) - { - user.Policy.IsDisabled = false; - UpdateUserPolicy(user, user.Policy, true); - } - usersReset.Add(user.Name); - } - } - else - { - _pinAttempts++; - if (_pinAttempts >= 3) - { - _lastPin = null; - _lastPasswordPinCreationResult = null; - } - } - - return new PinRedeemResult - { - Success = valid, - UsersReset = usersReset.ToArray() - }; - } - - private void DeletePinFile() - { - try - { - _fileSystem.DeleteFile(PasswordResetFile); - } - catch - { - - } - } - - class PasswordPinCreationResult - { - public string PinFile { get; set; } - public DateTime ExpirationDate { get; set; } - } - - public UserPolicy GetUserPolicy(User user) - { - var path = GetPolicyFilePath(user); - - if (!File.Exists(path)) - { - return GetDefaultPolicy(user); - } - - try - { - lock (_policySyncLock) - { - return (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), path); - } - } - catch (IOException) - { - return GetDefaultPolicy(user); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reading policy file: {path}", path); - - return GetDefaultPolicy(user); - } - } - - private static UserPolicy GetDefaultPolicy(User user) - { - return new UserPolicy - { - EnableContentDownloading = true, - EnableSyncTranscoding = true - }; - } - - private readonly object _policySyncLock = new object(); - public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy) - { - var user = GetUserById(userId); - UpdateUserPolicy(user, userPolicy, true); - } - - private void UpdateUserPolicy(User user, UserPolicy userPolicy, bool fireEvent) - { - // The xml serializer will output differently if the type is not exact - if (userPolicy.GetType() != typeof(UserPolicy)) - { - var json = _jsonSerializer.SerializeToString(userPolicy); - userPolicy = _jsonSerializer.DeserializeFromString(json); - } - - var path = GetPolicyFilePath(user); - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - lock (_policySyncLock) - { - _xmlSerializer.SerializeToFile(userPolicy, path); - user.Policy = userPolicy; - } - - if (fireEvent) - { - UserPolicyUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); - } - } - - private void DeleteUserPolicy(User user) - { - var path = GetPolicyFilePath(user); - - try - { - lock (_policySyncLock) - { - _fileSystem.DeleteFile(path); - } - } - catch (IOException) - { - - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting policy file"); - } - } - - private static string GetPolicyFilePath(User user) - { - return Path.Combine(user.ConfigurationDirectoryPath, "policy.xml"); - } - - private static string GetConfigurationFilePath(User user) - { - return Path.Combine(user.ConfigurationDirectoryPath, "config.xml"); - } - - public UserConfiguration GetUserConfiguration(User user) - { - var path = GetConfigurationFilePath(user); - - if (!File.Exists(path)) - { - return new UserConfiguration(); - } - - try - { - lock (_configSyncLock) - { - return (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), path); - } - } - catch (IOException) - { - return new UserConfiguration(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reading policy file: {path}", path); - - return new UserConfiguration(); - } - } - - private readonly object _configSyncLock = new object(); - public void UpdateConfiguration(Guid userId, UserConfiguration config) - { - var user = GetUserById(userId); - UpdateConfiguration(user, config); - } - - public void UpdateConfiguration(User user, UserConfiguration config) - { - UpdateConfiguration(user, config, true); - } - - private void UpdateConfiguration(User user, UserConfiguration config, bool fireEvent) - { - var path = GetConfigurationFilePath(user); - - // The xml serializer will output differently if the type is not exact - if (config.GetType() != typeof(UserConfiguration)) - { - var json = _jsonSerializer.SerializeToString(config); - config = _jsonSerializer.DeserializeFromString(json); - } - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - lock (_configSyncLock) - { - _xmlSerializer.SerializeToFile(config, path); - user.Configuration = config; - } - - if (fireEvent) - { - UserConfigurationUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); - } - } - } - - public class DeviceAccessEntryPoint : IServerEntryPoint - { - private IUserManager _userManager; - private IAuthenticationRepository _authRepo; - private IDeviceManager _deviceManager; - private ISessionManager _sessionManager; - - public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager) - { - _userManager = userManager; - _authRepo = authRepo; - _deviceManager = deviceManager; - _sessionManager = sessionManager; - } - - public Task RunAsync() - { - _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated; - - return Task.CompletedTask; - } - - private void _userManager_UserPolicyUpdated(object sender, GenericEventArgs e) - { - var user = e.Argument; - if (!user.Policy.EnableAllDevices) - { - UpdateDeviceAccess(user); - } - } - - private void UpdateDeviceAccess(User user) - { - var existing = _authRepo.Get(new AuthenticationInfoQuery - { - UserId = user.Id - - }).Items; - - foreach (var authInfo in existing) - { - if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId)) - { - _sessionManager.Logout(authInfo); - } - } - } - - public void Dispose() - { - - } - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Events; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Users; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.Library +{ + /// + /// Class UserManager + /// + public class UserManager : IUserManager + { + /// + /// Gets the users. + /// + /// The users. + public IEnumerable Users => _users; + + private User[] _users; + + /// + /// The _logger + /// + private readonly ILogger _logger; + + /// + /// Gets or sets the configuration manager. + /// + /// The configuration manager. + private IServerConfigurationManager ConfigurationManager { get; set; } + + /// + /// Gets the active user repository + /// + /// The user repository. + private IUserRepository UserRepository { get; set; } + public event EventHandler> UserPasswordChanged; + + private readonly IXmlSerializer _xmlSerializer; + private readonly IJsonSerializer _jsonSerializer; + + private readonly INetworkManager _networkManager; + + private readonly Func _imageProcessorFactory; + private readonly Func _dtoServiceFactory; + private readonly IServerApplicationHost _appHost; + private readonly IFileSystem _fileSystem; + + private IAuthenticationProvider[] _authenticationProviders; + private DefaultAuthenticationProvider _defaultAuthenticationProvider; + + public UserManager( + ILoggerFactory loggerFactory, + IServerConfigurationManager configurationManager, + IUserRepository userRepository, + IXmlSerializer xmlSerializer, + INetworkManager networkManager, + Func imageProcessorFactory, + Func dtoServiceFactory, + IServerApplicationHost appHost, + IJsonSerializer jsonSerializer, + IFileSystem fileSystem) + { + _logger = loggerFactory.CreateLogger(nameof(UserManager)); + UserRepository = userRepository; + _xmlSerializer = xmlSerializer; + _networkManager = networkManager; + _imageProcessorFactory = imageProcessorFactory; + _dtoServiceFactory = dtoServiceFactory; + _appHost = appHost; + _jsonSerializer = jsonSerializer; + _fileSystem = fileSystem; + ConfigurationManager = configurationManager; + _users = Array.Empty(); + + DeletePinFile(); + } + + public NameIdPair[] GetAuthenticationProviders() + { + return _authenticationProviders + .Where(i => i.IsEnabled) + .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1) + .ThenBy(i => i.Name) + .Select(i => new NameIdPair + { + Name = i.Name, + Id = GetAuthenticationProviderId(i) + }) + .ToArray(); + } + + public void AddParts(IEnumerable authenticationProviders) + { + _authenticationProviders = authenticationProviders.ToArray(); + + _defaultAuthenticationProvider = _authenticationProviders.OfType().First(); + } + + #region UserUpdated Event + /// + /// Occurs when [user updated]. + /// + public event EventHandler> UserUpdated; + public event EventHandler> UserPolicyUpdated; + public event EventHandler> UserConfigurationUpdated; + public event EventHandler> UserLockedOut; + + /// + /// Called when [user updated]. + /// + /// The user. + private void OnUserUpdated(User user) + { + UserUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); + } + #endregion + + #region UserDeleted Event + /// + /// Occurs when [user deleted]. + /// + public event EventHandler> UserDeleted; + /// + /// Called when [user deleted]. + /// + /// The user. + private void OnUserDeleted(User user) + { + UserDeleted?.Invoke(this, new GenericEventArgs { Argument = user }); + } + #endregion + + /// + /// Gets a User by Id + /// + /// The id. + /// User. + /// + public User GetUserById(Guid id) + { + if (id == Guid.Empty) + { + throw new ArgumentException(nameof(id), "Guid can't be empty"); + } + + return Users.FirstOrDefault(u => u.Id == id); + } + + /// + /// Gets the user by identifier. + /// + /// The identifier. + /// User. + public User GetUserById(string id) + { + return GetUserById(new Guid(id)); + } + + public User GetUserByName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase)); + } + + public void Initialize() + { + _users = LoadUsers(); + + var users = Users.ToList(); + + // If there are no local users with admin rights, make them all admins + if (!users.Any(i => i.Policy.IsAdministrator)) + { + foreach (var user in users) + { + user.Policy.IsAdministrator = true; + UpdateUserPolicy(user, user.Policy, false); + } + } + } + + public static bool IsValidUsername(string username) + { + // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ + // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness + // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.) + return Regex.IsMatch(username, @"^[\w\-'._@]*$"); + } + + private static bool IsValidUsernameCharacter(char i) + { + return IsValidUsername(i.ToString()); + } + + public string MakeValidUsername(string username) + { + if (IsValidUsername(username)) + { + return username; + } + + // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) + var builder = new StringBuilder(); + + foreach (var c in username) + { + if (IsValidUsernameCharacter(c)) + { + builder.Append(c); + } + } + return builder.ToString(); + } + + public async Task AuthenticateUser(string username, string password, string hashedPassword, string remoteEndPoint, bool isUserSession) + { + if (string.IsNullOrWhiteSpace(username)) + { + throw new ArgumentNullException(nameof(username)); + } + + var user = Users + .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); + + var success = false; + IAuthenticationProvider authenticationProvider = null; + + if (user != null) + { + var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false); + authenticationProvider = authResult.Item1; + success = authResult.Item2; + } + else + { + // user is null + var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); + authenticationProvider = authResult.Item1; + success = authResult.Item2; + + if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) + { + user = await CreateUser(username).ConfigureAwait(false); + + var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy; + if (hasNewUserPolicy != null) + { + var policy = hasNewUserPolicy.GetNewUserPolicy(); + UpdateUserPolicy(user, policy, true); + } + } + } + + if (success && user != null && authenticationProvider != null) + { + var providerId = GetAuthenticationProviderId(authenticationProvider); + + if (!string.Equals(providerId, user.Policy.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) + { + user.Policy.AuthenticationProviderId = providerId; + UpdateUserPolicy(user, user.Policy, true); + } + } + + if (user == null) + { + throw new SecurityException("Invalid username or password entered."); + } + + if (user.Policy.IsDisabled) + { + throw new SecurityException(string.Format("The {0} account is currently disabled. Please consult with your administrator.", user.Name)); + } + + if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint)) + { + throw new SecurityException("Forbidden."); + } + + if (!user.IsParentalScheduleAllowed()) + { + throw new SecurityException("User is not allowed access at this time."); + } + + // Update LastActivityDate and LastLoginDate, then save + if (success) + { + if (isUserSession) + { + user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; + UpdateUser(user); + } + UpdateInvalidLoginAttemptCount(user, 0); + } + else + { + UpdateInvalidLoginAttemptCount(user, user.Policy.InvalidLoginAttemptCount + 1); + } + + _logger.LogInformation("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied"); + + return success ? user : null; + } + + private static string GetAuthenticationProviderId(IAuthenticationProvider provider) + { + return provider.GetType().FullName; + } + + private IAuthenticationProvider GetAuthenticationProvider(User user) + { + return GetAuthenticationProviders(user).First(); + } + + private IAuthenticationProvider[] GetAuthenticationProviders(User user) + { + var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId; + + var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray(); + + if (!string.IsNullOrEmpty(authenticationProviderId)) + { + providers = providers.Where(i => string.Equals(authenticationProviderId, GetAuthenticationProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray(); + } + + if (providers.Length == 0) + { + providers = new IAuthenticationProvider[] { _defaultAuthenticationProvider }; + } + + return providers; + } + + private async Task AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) + { + try + { + var requiresResolvedUser = provider as IRequiresResolvedUser; + if (requiresResolvedUser != null) + { + await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); + } + else + { + await provider.Authenticate(username, password).ConfigureAwait(false); + } + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name); + + return false; + } + } + + private async Task> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) + { + bool success = false; + IAuthenticationProvider authenticationProvider = null; + + if (password != null && user != null) + { + // Doesn't look like this is even possible to be used, because of password == null checks below + hashedPassword = _defaultAuthenticationProvider.GetHashedString(user, password); + } + + if (password == null) + { + // legacy + success = string.Equals(_defaultAuthenticationProvider.GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + } + else + { + foreach (var provider in GetAuthenticationProviders(user)) + { + success = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); + + if (success) + { + authenticationProvider = provider; + break; + } + } + } + + if (user != null) + { + if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword) + { + if (password == null) + { + // legacy + success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + } + else + { + success = string.Equals(GetLocalPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); + } + } + } + + return new Tuple(authenticationProvider, success); + } + + private void UpdateInvalidLoginAttemptCount(User user, int newValue) + { + if (user.Policy.InvalidLoginAttemptCount == newValue || newValue <= 0) + { + return; + } + + user.Policy.InvalidLoginAttemptCount = newValue; + + // Check for users without a value here and then fill in the default value + // also protect from an always lockout if misconfigured + if (user.Policy.LoginAttemptsBeforeLockout == null || user.Policy.LoginAttemptsBeforeLockout == 0) + { + user.Policy.LoginAttemptsBeforeLockout = user.Policy.IsAdministrator ? 5 : 3; + } + + var maxCount = user.Policy.LoginAttemptsBeforeLockout; + + var fireLockout = false; + + // -1 can be used to specify no lockout value + if (maxCount != -1 && newValue >= maxCount) + { + _logger.LogDebug("Disabling user {0} due to {1} unsuccessful login attempts.", user.Name, newValue); + user.Policy.IsDisabled = true; + + fireLockout = true; + } + + UpdateUserPolicy(user, user.Policy, false); + + if (fireLockout) + { + UserLockedOut?.Invoke(this, new GenericEventArgs(user)); + } + } + + private string GetLocalPasswordHash(User user) + { + return string.IsNullOrEmpty(user.EasyPassword) + ? null + : user.EasyPassword; + } + + /// + /// Loads the users from the repository + /// + /// IEnumerable{User}. + private User[] LoadUsers() + { + var users = UserRepository.RetrieveAllUsers(); + + // There always has to be at least one user. + if (users.Count == 0) + { + var defaultName = Environment.UserName; + if (string.IsNullOrWhiteSpace(defaultName)) + { + defaultName = "MyJellyfinUser"; + } + var name = MakeValidUsername(defaultName); + + var user = InstantiateNewUser(name); + + user.DateLastSaved = DateTime.UtcNow; + + UserRepository.CreateUser(user); + + users.Add(user); + + user.Policy.IsAdministrator = true; + user.Policy.EnableContentDeletion = true; + user.Policy.EnableRemoteControlOfOtherUsers = true; + UpdateUserPolicy(user, user.Policy, false); + } + + return users.ToArray(); + } + + public UserDto GetUserDto(User user, string remoteEndPoint = null) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; + bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user)); + + bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? + hasConfiguredEasyPassword : + hasConfiguredPassword; + + UserDto dto = new UserDto + { + Id = user.Id, + Name = user.Name, + HasPassword = hasPassword, + HasConfiguredPassword = hasConfiguredPassword, + HasConfiguredEasyPassword = hasConfiguredEasyPassword, + LastActivityDate = user.LastActivityDate, + LastLoginDate = user.LastLoginDate, + Configuration = user.Configuration, + ServerId = _appHost.SystemId, + Policy = user.Policy + }; + + if (!hasPassword && Users.Count() == 1) + { + dto.EnableAutoLogin = true; + } + + ItemImageInfo image = user.GetImageInfo(ImageType.Primary, 0); + + if (image != null) + { + dto.PrimaryImageTag = GetImageCacheTag(user, image); + + try + { + _dtoServiceFactory().AttachPrimaryImageAspectRatio(dto, user); + } + catch (Exception ex) + { + // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions + _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {user}", user.Name); + } + } + + return dto; + } + + public UserDto GetOfflineUserDto(User user) + { + var dto = GetUserDto(user); + + dto.ServerName = _appHost.FriendlyName; + + return dto; + } + + private string GetImageCacheTag(BaseItem item, ItemImageInfo image) + { + try + { + return _imageProcessorFactory().GetImageCacheTag(item, image); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting {imageType} image info for {imagePath}", image.Type, image.Path); + return null; + } + } + + /// + /// Refreshes metadata for each user + /// + /// The cancellation token. + /// Task. + public async Task RefreshUsersMetadata(CancellationToken cancellationToken) + { + foreach (var user in Users) + { + await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Renames the user. + /// + /// The user. + /// The new name. + /// Task. + /// user + /// + public async Task RenameUser(User user, string newName) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (string.IsNullOrEmpty(newName)) + { + throw new ArgumentNullException(nameof(newName)); + } + + if (Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName)); + } + + if (user.Name.Equals(newName, StringComparison.Ordinal)) + { + throw new ArgumentException("The new and old names must be different."); + } + + await user.Rename(newName); + + OnUserUpdated(user); + } + + /// + /// Updates the user. + /// + /// The user. + /// user + /// + public void UpdateUser(User user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.Id.Equals(Guid.Empty) || !Users.Any(u => u.Id.Equals(user.Id))) + { + throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id)); + } + + user.DateModified = DateTime.UtcNow; + user.DateLastSaved = DateTime.UtcNow; + + UserRepository.UpdateUser(user); + + OnUserUpdated(user); + } + + public event EventHandler> UserCreated; + + private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1); + + /// + /// Creates the user. + /// + /// The name. + /// User. + /// name + /// + public async Task CreateUser(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + if (!IsValidUsername(name)) + { + throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); + } + + if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name)); + } + + await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); + + try + { + var user = InstantiateNewUser(name); + + var list = Users.ToList(); + list.Add(user); + _users = list.ToArray(); + + user.DateLastSaved = DateTime.UtcNow; + + UserRepository.CreateUser(user); + + EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs { Argument = user }, _logger); + + return user; + } + finally + { + _userListLock.Release(); + } + } + + /// + /// Deletes the user. + /// + /// The user. + /// Task. + /// user + /// + public async Task DeleteUser(User user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var allUsers = Users.ToList(); + + if (allUsers.FirstOrDefault(u => u.Id == user.Id) == null) + { + throw new ArgumentException(string.Format("The user cannot be deleted because there is no user with the Name {0} and Id {1}.", user.Name, user.Id)); + } + + if (allUsers.Count == 1) + { + throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one user in the system.", user.Name)); + } + + if (user.Policy.IsAdministrator && allUsers.Count(i => i.Policy.IsAdministrator) == 1) + { + throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one admin user in the system.", user.Name)); + } + + await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); + + try + { + var configPath = GetConfigurationFilePath(user); + + UserRepository.DeleteUser(user); + + try + { + _fileSystem.DeleteFile(configPath); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting file {path}", configPath); + } + + DeleteUserPolicy(user); + + _users = allUsers.Where(i => i.Id != user.Id).ToArray(); + + OnUserDeleted(user); + } + finally + { + _userListLock.Release(); + } + } + + /// + /// Resets the password by clearing it. + /// + /// Task. + public Task ResetPassword(User user) + { + return ChangePassword(user, string.Empty); + } + + public void ResetEasyPassword(User user) + { + ChangeEasyPassword(user, string.Empty, null); + } + + public async Task ChangePassword(User user, string newPassword) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); + + UpdateUser(user); + + UserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); + } + + public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (newPassword != null) + { + newPasswordHash = _defaultAuthenticationProvider.GetHashedString(user, newPassword); + } + + if (string.IsNullOrWhiteSpace(newPasswordHash)) + { + throw new ArgumentNullException(nameof(newPasswordHash)); + } + + user.EasyPassword = newPasswordHash; + + UpdateUser(user); + + UserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); + } + + /// + /// Instantiates the new user. + /// + /// The name. + /// User. + private static User InstantiateNewUser(string name) + { + return new User + { + Name = name, + Id = Guid.NewGuid(), + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + UsesIdForConfigurationPath = true, + //Salt = BCrypt.GenerateSalt() + }; + } + + private string PasswordResetFile => Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "passwordreset.txt"); + + private string _lastPin; + private PasswordPinCreationResult _lastPasswordPinCreationResult; + private int _pinAttempts; + + private async Task CreatePasswordResetPin() + { + var num = new Random().Next(1, 9999); + + var path = PasswordResetFile; + + var pin = num.ToString("0000", CultureInfo.InvariantCulture); + _lastPin = pin; + + var time = TimeSpan.FromMinutes(5); + var expiration = DateTime.UtcNow.Add(time); + + var text = new StringBuilder(); + + var localAddress = (await _appHost.GetLocalApiUrl(CancellationToken.None).ConfigureAwait(false)) ?? string.Empty; + + text.AppendLine("Use your web browser to visit:"); + text.AppendLine(string.Empty); + text.AppendLine(localAddress + "/web/index.html#!/forgotpasswordpin.html"); + text.AppendLine(string.Empty); + text.AppendLine("Enter the following pin code:"); + text.AppendLine(string.Empty); + text.AppendLine(pin); + text.AppendLine(string.Empty); + + var localExpirationTime = expiration.ToLocalTime(); + // Tuesday, 22 August 2006 06:30 AM + text.AppendLine("The pin code will expire at " + localExpirationTime.ToString("f1", CultureInfo.CurrentCulture)); + + File.WriteAllText(path, text.ToString(), Encoding.UTF8); + + var result = new PasswordPinCreationResult + { + PinFile = path, + ExpirationDate = expiration + }; + + _lastPasswordPinCreationResult = result; + _pinAttempts = 0; + + return result; + } + + public async Task StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) + { + DeletePinFile(); + + var user = string.IsNullOrWhiteSpace(enteredUsername) ? + null : + GetUserByName(enteredUsername); + + var action = ForgotPasswordAction.InNetworkRequired; + string pinFile = null; + DateTime? expirationDate = null; + + if (user != null && !user.Policy.IsAdministrator) + { + action = ForgotPasswordAction.ContactAdmin; + } + else + { + if (isInNetwork) + { + action = ForgotPasswordAction.PinCode; + } + + var result = await CreatePasswordResetPin().ConfigureAwait(false); + pinFile = result.PinFile; + expirationDate = result.ExpirationDate; + } + + return new ForgotPasswordResult + { + Action = action, + PinFile = pinFile, + PinExpirationDate = expirationDate + }; + } + + public async Task RedeemPasswordResetPin(string pin) + { + DeletePinFile(); + + var usersReset = new List(); + + var valid = !string.IsNullOrWhiteSpace(_lastPin) && + string.Equals(_lastPin, pin, StringComparison.OrdinalIgnoreCase) && + _lastPasswordPinCreationResult != null && + _lastPasswordPinCreationResult.ExpirationDate > DateTime.UtcNow; + + if (valid) + { + _lastPin = null; + _lastPasswordPinCreationResult = null; + + foreach (var user in Users) + { + await ResetPassword(user).ConfigureAwait(false); + + if (user.Policy.IsDisabled) + { + user.Policy.IsDisabled = false; + UpdateUserPolicy(user, user.Policy, true); + } + usersReset.Add(user.Name); + } + } + else + { + _pinAttempts++; + if (_pinAttempts >= 3) + { + _lastPin = null; + _lastPasswordPinCreationResult = null; + } + } + + return new PinRedeemResult + { + Success = valid, + UsersReset = usersReset.ToArray() + }; + } + + private void DeletePinFile() + { + try + { + _fileSystem.DeleteFile(PasswordResetFile); + } + catch + { + + } + } + + class PasswordPinCreationResult + { + public string PinFile { get; set; } + public DateTime ExpirationDate { get; set; } + } + + public UserPolicy GetUserPolicy(User user) + { + var path = GetPolicyFilePath(user); + + if (!File.Exists(path)) + { + return GetDefaultPolicy(user); + } + + try + { + lock (_policySyncLock) + { + return (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), path); + } + } + catch (IOException) + { + return GetDefaultPolicy(user); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error reading policy file: {path}", path); + + return GetDefaultPolicy(user); + } + } + + private static UserPolicy GetDefaultPolicy(User user) + { + return new UserPolicy + { + EnableContentDownloading = true, + EnableSyncTranscoding = true + }; + } + + private readonly object _policySyncLock = new object(); + public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy) + { + var user = GetUserById(userId); + UpdateUserPolicy(user, userPolicy, true); + } + + private void UpdateUserPolicy(User user, UserPolicy userPolicy, bool fireEvent) + { + // The xml serializer will output differently if the type is not exact + if (userPolicy.GetType() != typeof(UserPolicy)) + { + var json = _jsonSerializer.SerializeToString(userPolicy); + userPolicy = _jsonSerializer.DeserializeFromString(json); + } + + var path = GetPolicyFilePath(user); + + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + lock (_policySyncLock) + { + _xmlSerializer.SerializeToFile(userPolicy, path); + user.Policy = userPolicy; + } + + if (fireEvent) + { + UserPolicyUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); + } + } + + private void DeleteUserPolicy(User user) + { + var path = GetPolicyFilePath(user); + + try + { + lock (_policySyncLock) + { + _fileSystem.DeleteFile(path); + } + } + catch (IOException) + { + + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting policy file"); + } + } + + private static string GetPolicyFilePath(User user) + { + return Path.Combine(user.ConfigurationDirectoryPath, "policy.xml"); + } + + private static string GetConfigurationFilePath(User user) + { + return Path.Combine(user.ConfigurationDirectoryPath, "config.xml"); + } + + public UserConfiguration GetUserConfiguration(User user) + { + var path = GetConfigurationFilePath(user); + + if (!File.Exists(path)) + { + return new UserConfiguration(); + } + + try + { + lock (_configSyncLock) + { + return (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), path); + } + } + catch (IOException) + { + return new UserConfiguration(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error reading policy file: {path}", path); + + return new UserConfiguration(); + } + } + + private readonly object _configSyncLock = new object(); + public void UpdateConfiguration(Guid userId, UserConfiguration config) + { + var user = GetUserById(userId); + UpdateConfiguration(user, config); + } + + public void UpdateConfiguration(User user, UserConfiguration config) + { + UpdateConfiguration(user, config, true); + } + + private void UpdateConfiguration(User user, UserConfiguration config, bool fireEvent) + { + var path = GetConfigurationFilePath(user); + + // The xml serializer will output differently if the type is not exact + if (config.GetType() != typeof(UserConfiguration)) + { + var json = _jsonSerializer.SerializeToString(config); + config = _jsonSerializer.DeserializeFromString(json); + } + + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + lock (_configSyncLock) + { + _xmlSerializer.SerializeToFile(config, path); + user.Configuration = config; + } + + if (fireEvent) + { + UserConfigurationUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); + } + } + } + + public class DeviceAccessEntryPoint : IServerEntryPoint + { + private IUserManager _userManager; + private IAuthenticationRepository _authRepo; + private IDeviceManager _deviceManager; + private ISessionManager _sessionManager; + + public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager) + { + _userManager = userManager; + _authRepo = authRepo; + _deviceManager = deviceManager; + _sessionManager = sessionManager; + } + + public Task RunAsync() + { + _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated; + + return Task.CompletedTask; + } + + private void _userManager_UserPolicyUpdated(object sender, GenericEventArgs e) + { + var user = e.Argument; + if (!user.Policy.EnableAllDevices) + { + UpdateDeviceAccess(user); + } + } + + private void UpdateDeviceAccess(User user) + { + var existing = _authRepo.Get(new AuthenticationInfoQuery + { + UserId = user.Id + + }).Items; + + foreach (var authInfo in existing) + { + if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId)) + { + _sessionManager.Logout(authInfo); + } + } + } + + public void Dispose() + { + + } + } +} -- cgit v1.2.3 From 09921a00aaad31c0ea4a0650e8d0ddb890dca735 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Fri, 22 Mar 2019 00:01:23 -0700 Subject: made password resets an interface and per user --- Emby.Server.Implementations/ApplicationHost.cs | 2 +- .../Library/DefaultPasswordResetProvider.cs | 118 +++++++++++++ Emby.Server.Implementations/Library/UserManager.cs | 192 +++++++-------------- MediaBrowser.Api/Session/SessionsService.cs | 11 ++ .../Authentication/IPasswordResetProvider.cs | 20 +++ MediaBrowser.Controller/Library/IUserManager.cs | 3 +- MediaBrowser.Model/Users/UserPolicy.cs | 1 + 7 files changed, 220 insertions(+), 127 deletions(-) create mode 100644 Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs create mode 100644 MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 484942946..fc1b2eda8 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1088,7 +1088,7 @@ namespace Emby.Server.Implementations MediaSourceManager.AddParts(GetExports()); NotificationManager.AddParts(GetExports(), GetExports()); - UserManager.AddParts(GetExports()); + UserManager.AddParts(GetExports(), GetExports()); IsoManager.AddParts(GetExports()); } diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs new file mode 100644 index 000000000..ae6fe8239 --- /dev/null +++ b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Users; +using ServiceStack; +using TvDbSharper.Dto; + +namespace Emby.Server.Implementations.Library +{ + public class DefaultPasswordResetProvider : IPasswordResetProvider + { + public string Name => "Default Password Reset Provider"; + + public bool IsEnabled => true; + + private readonly string _passwordResetFileBase; + private readonly string _passwordResetFileBaseDir; + private readonly string _passwordResetFileBaseName = "passwordreset"; + + private IJsonSerializer _jsonSerializer; + private IUserManager _userManager; + + public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager) + { + _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; + _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName); + _jsonSerializer = jsonSerializer; + _userManager = userManager; + } + + public async Task RedeemPasswordResetPin(string pin) + { + HashSet usersreset = new HashSet(); + foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*")) + { + var spr = (SerializablePasswordReset) _jsonSerializer.DeserializeFromFile(typeof(SerializablePasswordReset), resetfile); + if (spr.ExpirationDate > DateTime.Now) + { + File.Delete(resetfile); + } + else + { + if (spr.Pin == pin) + { + var resetUser = _userManager.GetUserByName(spr.UserName); + if (!string.IsNullOrEmpty(resetUser.Password)) + { + await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); + usersreset.Add(resetUser.Name); + } + } + } + } + + if (usersreset.Count < 1) + { + throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}"); + } + else + { + return new PinRedeemResult + { + Success = true, + UsersReset = usersreset.ToArray() + }; + } + throw new System.NotImplementedException(); + } + + public async Task StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork) + { + string pin = new Random().Next(99999999).ToString("00000000",CultureInfo.InvariantCulture); + DateTime expireTime = DateTime.Now.AddMinutes(30); + string filePath = _passwordResetFileBase + user.Name.ToLowerInvariant() + ".json"; + SerializablePasswordReset spr = new SerializablePasswordReset + { + ExpirationDate = expireTime, + Pin = pin, + PinFile = filePath, + UserName = user.Name + }; + + try + { + await Task.Run(() => File.WriteAllText(filePath, _jsonSerializer.SerializeToString(spr))).ConfigureAwait(false); + } + catch (Exception e) + { + throw new Exception($"Error serializing or writing password reset for {user.Name} to location:{filePath}", e); + } + + return new ForgotPasswordResult + { + Action = ForgotPasswordAction.PinCode, + PinExpirationDate = expireTime, + PinFile = filePath + }; + } + + private class SerializablePasswordReset : PasswordPinCreationResult + { + public string Pin { get; set; } + + public string UserName { get; set; } + } + } +} diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 4cf703add..500bb8d66 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -79,6 +79,10 @@ namespace Emby.Server.Implementations.Library private IAuthenticationProvider[] _authenticationProviders; private DefaultAuthenticationProvider _defaultAuthenticationProvider; + private IPasswordResetProvider[] _passwordResetProviders; + private DefaultPasswordResetProvider _defaultPasswordResetProvider; + private Dictionary _activeResets = new Dictionary(); + public UserManager( ILoggerFactory loggerFactory, IServerConfigurationManager configurationManager, @@ -102,8 +106,6 @@ namespace Emby.Server.Implementations.Library _fileSystem = fileSystem; ConfigurationManager = configurationManager; _users = Array.Empty(); - - DeletePinFile(); } public NameIdPair[] GetAuthenticationProviders() @@ -120,11 +122,29 @@ namespace Emby.Server.Implementations.Library .ToArray(); } - public void AddParts(IEnumerable authenticationProviders) + public NameIdPair[] GetPasswordResetProviders() + { + return _passwordResetProviders + .Where(i => i.IsEnabled) + .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1) + .ThenBy(i => i.Name) + .Select(i => new NameIdPair + { + Name = i.Name, + Id = GetPasswordResetProviderId(i) + }) + .ToArray(); + } + + public void AddParts(IEnumerable authenticationProviders,IEnumerable passwordResetProviders) { _authenticationProviders = authenticationProviders.ToArray(); _defaultAuthenticationProvider = _authenticationProviders.OfType().First(); + + _passwordResetProviders = passwordResetProviders.ToArray(); + + _defaultPasswordResetProvider = passwordResetProviders.OfType().First(); } #region UserUpdated Event @@ -342,11 +362,21 @@ namespace Emby.Server.Implementations.Library return provider.GetType().FullName; } + private static string GetPasswordResetProviderId(IPasswordResetProvider provider) + { + return provider.GetType().FullName; + } + private IAuthenticationProvider GetAuthenticationProvider(User user) { return GetAuthenticationProviders(user).First(); } + private IPasswordResetProvider GetPasswordResetProvider(User user) + { + return GetPasswordResetProviders(user).First(); + } + private IAuthenticationProvider[] GetAuthenticationProviders(User user) { var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId; @@ -366,6 +396,25 @@ namespace Emby.Server.Implementations.Library return providers; } + private IPasswordResetProvider[] GetPasswordResetProviders(User user) + { + var passwordResetProviderId = user == null ? null : user.Policy.PasswordResetProviderId; + + var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray(); + + if (!string.IsNullOrEmpty(passwordResetProviderId)) + { + providers = providers.Where(i => string.Equals(passwordResetProviderId, GetPasswordResetProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray(); + } + + if (providers.Length == 0) + { + providers = new IPasswordResetProvider[] { _defaultPasswordResetProvider }; + } + + return providers; + } + private async Task AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) { try @@ -844,159 +893,52 @@ namespace Emby.Server.Implementations.Library Id = Guid.NewGuid(), DateCreated = DateTime.UtcNow, DateModified = DateTime.UtcNow, - UsesIdForConfigurationPath = true, - //Salt = BCrypt.GenerateSalt() - }; - } - - private string PasswordResetFile => Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "passwordreset.txt"); - - private string _lastPin; - private PasswordPinCreationResult _lastPasswordPinCreationResult; - private int _pinAttempts; - - private async Task CreatePasswordResetPin() - { - var num = new Random().Next(1, 9999); - - var path = PasswordResetFile; - - var pin = num.ToString("0000", CultureInfo.InvariantCulture); - _lastPin = pin; - - var time = TimeSpan.FromMinutes(5); - var expiration = DateTime.UtcNow.Add(time); - - var text = new StringBuilder(); - - var localAddress = (await _appHost.GetLocalApiUrl(CancellationToken.None).ConfigureAwait(false)) ?? string.Empty; - - text.AppendLine("Use your web browser to visit:"); - text.AppendLine(string.Empty); - text.AppendLine(localAddress + "/web/index.html#!/forgotpasswordpin.html"); - text.AppendLine(string.Empty); - text.AppendLine("Enter the following pin code:"); - text.AppendLine(string.Empty); - text.AppendLine(pin); - text.AppendLine(string.Empty); - - var localExpirationTime = expiration.ToLocalTime(); - // Tuesday, 22 August 2006 06:30 AM - text.AppendLine("The pin code will expire at " + localExpirationTime.ToString("f1", CultureInfo.CurrentCulture)); - - File.WriteAllText(path, text.ToString(), Encoding.UTF8); - - var result = new PasswordPinCreationResult - { - PinFile = path, - ExpirationDate = expiration + UsesIdForConfigurationPath = true }; - - _lastPasswordPinCreationResult = result; - _pinAttempts = 0; - - return result; } public async Task StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) { - DeletePinFile(); - var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername); var action = ForgotPasswordAction.InNetworkRequired; - string pinFile = null; - DateTime? expirationDate = null; - if (user != null && !user.Policy.IsAdministrator) + if (user != null && isInNetwork) { - action = ForgotPasswordAction.ContactAdmin; + var passwordResetProvider = GetPasswordResetProvider(user); + _activeResets.Add(user.Name, passwordResetProvider); + return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false); } else { - if (isInNetwork) + return new ForgotPasswordResult { - action = ForgotPasswordAction.PinCode; - } - - var result = await CreatePasswordResetPin().ConfigureAwait(false); - pinFile = result.PinFile; - expirationDate = result.ExpirationDate; + Action = action, + PinFile = string.Empty + }; } - - return new ForgotPasswordResult - { - Action = action, - PinFile = pinFile, - PinExpirationDate = expirationDate - }; } public async Task RedeemPasswordResetPin(string pin) { - DeletePinFile(); - - var usersReset = new List(); - - var valid = !string.IsNullOrWhiteSpace(_lastPin) && - string.Equals(_lastPin, pin, StringComparison.OrdinalIgnoreCase) && - _lastPasswordPinCreationResult != null && - _lastPasswordPinCreationResult.ExpirationDate > DateTime.UtcNow; - - if (valid) + foreach (var provider in _passwordResetProviders) { - _lastPin = null; - _lastPasswordPinCreationResult = null; - - foreach (var user in Users) + var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false); + if (result.Success) { - await ResetPassword(user).ConfigureAwait(false); - - if (user.Policy.IsDisabled) - { - user.Policy.IsDisabled = false; - UpdateUserPolicy(user, user.Policy, true); - } - usersReset.Add(user.Name); - } - } - else - { - _pinAttempts++; - if (_pinAttempts >= 3) - { - _lastPin = null; - _lastPasswordPinCreationResult = null; + return result; } } return new PinRedeemResult { - Success = valid, - UsersReset = usersReset.ToArray() + Success = false, + UsersReset = Array.Empty() }; } - private void DeletePinFile() - { - try - { - _fileSystem.DeleteFile(PasswordResetFile); - } - catch - { - - } - } - - class PasswordPinCreationResult - { - public string PinFile { get; set; } - public DateTime ExpirationDate { get; set; } - } - public UserPolicy GetUserPolicy(User user) { var path = GetPolicyFilePath(user); diff --git a/MediaBrowser.Api/Session/SessionsService.cs b/MediaBrowser.Api/Session/SessionsService.cs index f011e6e41..4109b12bf 100644 --- a/MediaBrowser.Api/Session/SessionsService.cs +++ b/MediaBrowser.Api/Session/SessionsService.cs @@ -245,6 +245,12 @@ namespace MediaBrowser.Api.Session { } + [Route("/Auth/PasswordResetProviders", "GET")] + [Authenticated(Roles = "Admin")] + public class GetPasswordResetProviders : IReturn + { + } + [Route("/Auth/Keys/{Key}", "DELETE")] [Authenticated(Roles = "Admin")] public class RevokeKey @@ -294,6 +300,11 @@ namespace MediaBrowser.Api.Session return _userManager.GetAuthenticationProviders(); } + public object Get(GetPasswordResetProviders request) + { + return _userManager.GetPasswordResetProviders(); + } + public void Delete(RevokeKey request) { _sessionManager.RevokeToken(request.Key); diff --git a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs new file mode 100644 index 000000000..9e5cd8816 --- /dev/null +++ b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Users; + +namespace MediaBrowser.Controller.Authentication +{ + public interface IPasswordResetProvider + { + string Name { get; } + bool IsEnabled { get; } + Task StartForgotPasswordProcess(User user, bool isInNetwork); + Task RedeemPasswordResetPin(string pin); + } + public class PasswordPinCreationResult + { + public string PinFile { get; set; } + public DateTime ExpirationDate { get; set; } + } +} diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index 925d91a37..7f7370893 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -200,8 +200,9 @@ namespace MediaBrowser.Controller.Library /// System.String. string MakeValidUsername(string username); - void AddParts(IEnumerable authenticationProviders); + void AddParts(IEnumerable authenticationProviders, IEnumerable passwordResetProviders); NameIdPair[] GetAuthenticationProviders(); + NameIdPair[] GetPasswordResetProviders(); } } diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 5415fd5e8..f63ab2bb4 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -75,6 +75,7 @@ namespace MediaBrowser.Model.Users public int RemoteClientBitrateLimit { get; set; } public string AuthenticationProviderId { get; set; } + public string PasswordResetProviderId { get; set; } public UserPolicy() { -- cgit v1.2.3 From 4e2841f0d747a9501d454fab7c7df5ce4ff86890 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sun, 24 Mar 2019 11:41:03 -0700 Subject: Update Emby.Server.Implementations/Library/UserManager.cs Co-Authored-By: LogicalPhallacy <44458166+LogicalPhallacy@users.noreply.github.com> --- Emby.Server.Implementations/Library/UserManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 500bb8d66..bddec70ed 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -398,7 +398,7 @@ namespace Emby.Server.Implementations.Library private IPasswordResetProvider[] GetPasswordResetProviders(User user) { - var passwordResetProviderId = user == null ? null : user.Policy.PasswordResetProviderId; + var passwordResetProviderId = user?.Policy.PasswordResetProviderId; var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray(); -- cgit v1.2.3 From 86772bd7bdd570264565c0078ddc66964860f389 Mon Sep 17 00:00:00 2001 From: Phallacy Date: Sun, 24 Mar 2019 12:17:32 -0700 Subject: removes needless dictionary --- Emby.Server.Implementations/Library/UserManager.cs | 2 -- 1 file changed, 2 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index bddec70ed..05ec750ba 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -81,7 +81,6 @@ namespace Emby.Server.Implementations.Library private IPasswordResetProvider[] _passwordResetProviders; private DefaultPasswordResetProvider _defaultPasswordResetProvider; - private Dictionary _activeResets = new Dictionary(); public UserManager( ILoggerFactory loggerFactory, @@ -908,7 +907,6 @@ namespace Emby.Server.Implementations.Library if (user != null && isInNetwork) { var passwordResetProvider = GetPasswordResetProvider(user); - _activeResets.Add(user.Name, passwordResetProvider); return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false); } else -- cgit v1.2.3 From 740c95d557515cedd3912983f7aec50bdfefb0d4 Mon Sep 17 00:00:00 2001 From: LogicalPhallacy <44458166+LogicalPhallacy@users.noreply.github.com> Date: Mon, 25 Mar 2019 21:40:10 -0700 Subject: Apply minor suggestions from code review Co-Authored-By: LogicalPhallacy <44458166+LogicalPhallacy@users.noreply.github.com> --- .../Library/DefaultPasswordResetProvider.cs | 223 +++++++++++---------- Emby.Server.Implementations/Library/UserManager.cs | 2 +- 2 files changed, 113 insertions(+), 112 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs index 1ae8960ee..46f3732d6 100644 --- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs @@ -1,113 +1,114 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; - -namespace Emby.Server.Implementations.Library -{ - public class DefaultPasswordResetProvider : IPasswordResetProvider - { - public string Name => "Default Password Reset Provider"; - - public bool IsEnabled => true; - - private readonly string _passwordResetFileBase; - private readonly string _passwordResetFileBaseDir; - private readonly string _passwordResetFileBaseName = "passwordreset"; - - private IJsonSerializer _jsonSerializer; - private IUserManager _userManager; - - public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager) - { - _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; - _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName); - _jsonSerializer = jsonSerializer; - _userManager = userManager; - } - - public async Task RedeemPasswordResetPin(string pin) - { - HashSet usersreset = new HashSet(); - foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*")) - { - var spr = (SerializablePasswordReset) _jsonSerializer.DeserializeFromFile(typeof(SerializablePasswordReset), resetfile); - if (spr.ExpirationDate < DateTime.Now) - { - File.Delete(resetfile); - } - else - { - if (spr.Pin == pin) - { - var resetUser = _userManager.GetUserByName(spr.UserName); - if (!string.IsNullOrEmpty(resetUser.Password)) - { - await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); - usersreset.Add(resetUser.Name); - } - } - } - } - - if (usersreset.Count < 1) - { - throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}"); - } - else - { - return new PinRedeemResult - { - Success = true, - UsersReset = usersreset.ToArray() - }; - } - throw new System.NotImplementedException(); - } - - public async Task StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork) - { - string pin = new Random().Next(99999999).ToString("00000000",CultureInfo.InvariantCulture); - DateTime expireTime = DateTime.Now.AddMinutes(30); - string filePath = _passwordResetFileBase + user.Name.ToLowerInvariant() + ".json"; - SerializablePasswordReset spr = new SerializablePasswordReset - { - ExpirationDate = expireTime, - Pin = pin, - PinFile = filePath, - UserName = user.Name - }; - - try - { - await Task.Run(() => File.WriteAllText(filePath, _jsonSerializer.SerializeToString(spr))).ConfigureAwait(false); - } - catch (Exception e) - { - throw new Exception($"Error serializing or writing password reset for {user.Name} to location:{filePath}", e); - } - - return new ForgotPasswordResult - { - Action = ForgotPasswordAction.PinCode, - PinExpirationDate = expireTime, - PinFile = filePath - }; - } - - private class SerializablePasswordReset : PasswordPinCreationResult - { - public string Pin { get; set; } +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Users; - public string UserName { get; set; } - } - } +namespace Emby.Server.Implementations.Library +{ + public class DefaultPasswordResetProvider : IPasswordResetProvider + { + public string Name => "Default Password Reset Provider"; + + public bool IsEnabled => true; + + private readonly string _passwordResetFileBase; + private readonly string _passwordResetFileBaseDir; + private readonly string _passwordResetFileBaseName = "passwordreset"; + + private IJsonSerializer _jsonSerializer; + private IUserManager _userManager; + + public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager) + { + _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; + _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName); + _jsonSerializer = jsonSerializer; + _userManager = userManager; + } + + public async Task RedeemPasswordResetPin(string pin) + { + HashSet usersreset = new HashSet(); + foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*")) + { + var spr = (SerializablePasswordReset) _jsonSerializer.DeserializeFromFile(typeof(SerializablePasswordReset), resetfile); + if (spr.ExpirationDate < DateTime.Now) + { + File.Delete(resetfile); + } + else + { + if (spr.Pin == pin) + { + var resetUser = _userManager.GetUserByName(spr.UserName); + if (!string.IsNullOrEmpty(resetUser.Password)) + { + await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); + usersreset.Add(resetUser.Name); + } + } + } + } + + if (usersreset.Count < 1) + { + throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}"); + } + else + { + return new PinRedeemResult + { + Success = true, + UsersReset = usersreset.ToArray() + }; + } + + throw new System.NotImplementedException(); + } + + public async Task StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork) + { + string pin = new Random().Next(99999999).ToString("00000000",CultureInfo.InvariantCulture); + DateTime expireTime = DateTime.Now.AddMinutes(30); + string filePath = _passwordResetFileBase + user.Name.ToLowerInvariant() + ".json"; + SerializablePasswordReset spr = new SerializablePasswordReset + { + ExpirationDate = expireTime, + Pin = pin, + PinFile = filePath, + UserName = user.Name + }; + + try + { + await Task.Run(() => File.WriteAllText(filePath, _jsonSerializer.SerializeToString(spr))).ConfigureAwait(false); + } + catch (Exception e) + { + throw new Exception($"Error serializing or writing password reset for {user.Name} to location: {filePath}", e); + } + + return new ForgotPasswordResult + { + Action = ForgotPasswordAction.PinCode, + PinExpirationDate = expireTime, + PinFile = filePath + }; + } + + private class SerializablePasswordReset : PasswordPinCreationResult + { + public string Pin { get; set; } + + public string UserName { get; set; } + } + } } diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 05ec750ba..75c82ca71 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -373,7 +373,7 @@ namespace Emby.Server.Implementations.Library private IPasswordResetProvider GetPasswordResetProvider(User user) { - return GetPasswordResetProviders(user).First(); + return GetPasswordResetProviders(user)[0]; } private IAuthenticationProvider[] GetAuthenticationProviders(User user) -- cgit v1.2.3 From 1af9c047fbc0283f7abfb4b98918454258dfb348 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Sun, 7 Apr 2019 19:51:45 -0400 Subject: Override username with AuthenticationProvider Pass back the Username directive returned by an AuthenticationProvider to the calling code, so we may override the user-provided Username value if the authentication provider passes this back. Useful for instance in an LDAP scenario where what the user types may not necessarily be the "username" that is mapped in the system, e.g. the user providing 'mail' while 'uid' is the "username" value. Could also then be extensible to other authentication providers as well, should they wish to do a similar thing. --- Emby.Server.Implementations/Library/UserManager.cs | 44 ++++++++++++++++------ 1 file changed, 33 insertions(+), 11 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 75c82ca71..952cc6896 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -277,24 +277,35 @@ namespace Emby.Server.Implementations.Library .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); var success = false; + string updatedUsername = null; IAuthenticationProvider authenticationProvider = null; if (user != null) { var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false); authenticationProvider = authResult.Item1; - success = authResult.Item2; + updatedUsername = authResult.Item2; + success = authResult.Item3; } else { // user is null var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); authenticationProvider = authResult.Item1; - success = authResult.Item2; + updatedUsername = authResult.Item2; + success = authResult.Item3; if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) { - user = await CreateUser(username).ConfigureAwait(false); + // We should trust the user that the authprovider says, not what was typed + if (updatedUsername != username) + { + username = updatedUsername; + } + + // Search the database for the user again; the authprovider might have created it + user = Users + .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy; if (hasNewUserPolicy != null) @@ -414,32 +425,40 @@ namespace Emby.Server.Implementations.Library return providers; } - private async Task AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) + private async Task> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) { try { var requiresResolvedUser = provider as IRequiresResolvedUser; + ProviderAuthenticationResult authenticationResult = null; if (requiresResolvedUser != null) { - await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); + authenticationResult = await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); } else { - await provider.Authenticate(username, password).ConfigureAwait(false); + authenticationResult = await provider.Authenticate(username, password).ConfigureAwait(false); + } + + if(authenticationResult.Username != username) + { + _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username); + username = authenticationResult.Username; } - return true; + return new Tuple(username, true); } catch (Exception ex) { _logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name); - return false; + return new Tuple(username, false); } } - private async Task> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) + private async Task> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) { + string updatedUsername = null; bool success = false; IAuthenticationProvider authenticationProvider = null; @@ -458,11 +477,14 @@ namespace Emby.Server.Implementations.Library { foreach (var provider in GetAuthenticationProviders(user)) { - success = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); + var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); + updatedUsername = providerAuthResult.Item1; + success = providerAuthResult.Item2; if (success) { authenticationProvider = provider; + username = updatedUsername; break; } } @@ -484,7 +506,7 @@ namespace Emby.Server.Implementations.Library } } - return new Tuple(authenticationProvider, success); + return new Tuple(authenticationProvider, username, success); } private void UpdateInvalidLoginAttemptCount(User user, int newValue) -- cgit v1.2.3 From 1df73fdeba0aca5ff2835080659877f0a6722f17 Mon Sep 17 00:00:00 2001 From: bugfixin Date: Tue, 30 Apr 2019 19:16:53 +0000 Subject: Fix incorrect hasPassword flag when easy pin set --- Emby.Server.Implementations/Library/UserManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 952cc6896..c33bb7740 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -596,7 +596,7 @@ namespace Emby.Server.Implementations.Library } bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; - bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user)); + bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetLocalPasswordHash(user)); bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? hasConfiguredEasyPassword : -- cgit v1.2.3 From c22068d6b1b84e54521d7ce31b3dac43eeb1e92e Mon Sep 17 00:00:00 2001 From: DrPandemic Date: Sat, 11 May 2019 19:32:20 -0400 Subject: Fix pin bug introduced in 10.3.z. The issue is that the new easyPassword format prepends the hash function. This PR extract the hash from "$SHA1$_hash_". --- .../Library/DefaultAuthenticationProvider.cs | 6 +++--- Emby.Server.Implementations/Library/UserManager.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 3d15a8afb..0527464ff 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.Library public string Name => "Default"; public bool IsEnabled => true; - + // This is dumb and an artifact of the backwards way auth providers were designed. // This version of authenticate was never meant to be called, but needs to be here for interface compat // Only the providers that don't provide local user support use this @@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Library { throw new NotImplementedException(); } - + // This is the verson that we need to use for local users. Because reasons. public Task Authenticate(string username, string password, User resolvedUser) { @@ -103,7 +103,7 @@ namespace Emby.Server.Implementations.Library string hash = user.Password; user.Password = string.Format("$SHA1${0}", hash); } - + if (user.EasyPassword != null && !user.EasyPassword.Contains("$")) { string hash = user.EasyPassword; diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index c33bb7740..b396ee51a 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -550,7 +550,7 @@ namespace Emby.Server.Implementations.Library { return string.IsNullOrEmpty(user.EasyPassword) ? null - : user.EasyPassword; + : (new PasswordHash(user.EasyPassword)).Hash; } /// -- cgit v1.2.3 From 69ee49bee607d716a857a1525f503575ebf6db7f Mon Sep 17 00:00:00 2001 From: DrPandemic Date: Sat, 25 May 2019 13:46:55 -0400 Subject: Format correctly the PIN when updating it --- .../Library/DefaultAuthenticationProvider.cs | 28 ++++++++++++++++++++++ Emby.Server.Implementations/Library/UserManager.cs | 27 ++++----------------- .../Authentication/IAuthenticationProvider.cs | 3 +++ 3 files changed, 36 insertions(+), 22 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 0527464ff..fe09b07ff 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -165,6 +165,34 @@ namespace Emby.Server.Implementations.Library return user.Password; } + public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) + { + ConvertPasswordFormat(user); + + if (newPassword != null) + { + newPasswordHash = string.Format("$SHA1${0}", GetHashedString(user, newPassword)); + } + + if (string.IsNullOrWhiteSpace(newPasswordHash)) + { + throw new ArgumentNullException(nameof(newPasswordHash)); + } + + user.EasyPassword = newPasswordHash; + } + + public string GetEasyPasswordHash(User user) + { + // This should be removed in the future. This was added to let user login after + // Jellyfin 10.3.3 failed to save a well formatted PIN. + ConvertPasswordFormat(user); + + return string.IsNullOrEmpty(user.EasyPassword) + ? null + : (new PasswordHash(user.EasyPassword)).Hash; + } + public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash) { passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword); diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index b396ee51a..a0b8d4ba4 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -471,7 +471,7 @@ namespace Emby.Server.Implementations.Library if (password == null) { // legacy - success = string.Equals(_defaultAuthenticationProvider.GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + success = string.Equals(GetAuthenticationProvider(user).GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); } else { @@ -497,11 +497,11 @@ namespace Emby.Server.Implementations.Library if (password == null) { // legacy - success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + success = string.Equals(GetAuthenticationProvider(user).GetEasyPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); } else { - success = string.Equals(GetLocalPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); + success = string.Equals(GetAuthenticationProvider(user).GetEasyPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); } } } @@ -546,13 +546,6 @@ namespace Emby.Server.Implementations.Library } } - private string GetLocalPasswordHash(User user) - { - return string.IsNullOrEmpty(user.EasyPassword) - ? null - : (new PasswordHash(user.EasyPassword)).Hash; - } - /// /// Loads the users from the repository /// @@ -596,7 +589,7 @@ namespace Emby.Server.Implementations.Library } bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; - bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetLocalPasswordHash(user)); + bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetAuthenticationProvider(user).GetEasyPasswordHash(user)); bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? hasConfiguredEasyPassword : @@ -884,17 +877,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(user)); } - if (newPassword != null) - { - newPasswordHash = _defaultAuthenticationProvider.GetHashedString(user, newPassword); - } - - if (string.IsNullOrWhiteSpace(newPasswordHash)) - { - throw new ArgumentNullException(nameof(newPasswordHash)); - } - - user.EasyPassword = newPasswordHash; + GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordHash); UpdateUser(user); diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs index b9f282bd2..2cf531eed 100644 --- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs +++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs @@ -11,6 +11,9 @@ namespace MediaBrowser.Controller.Authentication Task Authenticate(string username, string password); Task HasPassword(User user); Task ChangePassword(User user, string newPassword); + void ChangeEasyPassword(User user, string newPassword, string newPasswordHash); + string GetPasswordHash(User user); + string GetEasyPasswordHash(User user); } public interface IRequiresResolvedUser -- cgit v1.2.3 From d78a55adb4f66b8a82449216a11657da1388ab12 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sat, 8 Jun 2019 22:54:31 -0400 Subject: Implement InvalidAuthProvider Implements the InvalidAuthProvider, which acts as a fallback if a configured authentication provider, e.g. LDAP, is unavailable due to a load failure or removal. Until the user or the authentication plugin is corrected, this will cause users with the missing provider to be locked out, while throwing errors in the logs about the issue. Fixes #1445 part 2 --- .../Library/InvalidAuthProvider.cs | 46 ++++++++++++++++++++++ Emby.Server.Implementations/Library/UserManager.cs | 20 +++++++--- 2 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 Emby.Server.Implementations/Library/InvalidAuthProvider.cs (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/InvalidAuthProvider.cs b/Emby.Server.Implementations/Library/InvalidAuthProvider.cs new file mode 100644 index 000000000..ee2569562 --- /dev/null +++ b/Emby.Server.Implementations/Library/InvalidAuthProvider.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Entities; + +namespace Emby.Server.Implementations.Library +{ + public class InvalidAuthProvider : IAuthenticationProvider + { + public string Name => "InvalidorMissingAuthenticationProvider"; + + public bool IsEnabled => true; + + public Task Authenticate(string username, string password) + { + throw new Exception("User Account cannot login with this provider. The Normal provider for this user cannot be found"); + } + + public Task HasPassword(User user) + { + return Task.FromResult(true); + } + + public Task ChangePassword(User user, string newPassword) + { + return Task.FromResult(true); + } + + public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) + { + // Nothing here + } + + public string GetPasswordHash(User user) + { + return ""; + } + + public string GetEasyPasswordHash(User user) + { + return ""; + } + } +} diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index a0b8d4ba4..ca43f7aaa 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -79,6 +79,8 @@ namespace Emby.Server.Implementations.Library private IAuthenticationProvider[] _authenticationProviders; private DefaultAuthenticationProvider _defaultAuthenticationProvider; + private InvalidAuthProvider _invalidAuthProvider; + private IPasswordResetProvider[] _passwordResetProviders; private DefaultPasswordResetProvider _defaultPasswordResetProvider; @@ -141,6 +143,8 @@ namespace Emby.Server.Implementations.Library _defaultAuthenticationProvider = _authenticationProviders.OfType().First(); + _invalidAuthProvider = _authenticationProviders.OfType().First(); + _passwordResetProviders = passwordResetProviders.ToArray(); _defaultPasswordResetProvider = passwordResetProviders.OfType().First(); @@ -307,11 +311,14 @@ namespace Emby.Server.Implementations.Library user = Users .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); - var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy; - if (hasNewUserPolicy != null) + if (authenticationProvider.GetType() != typeof(InvalidAuthProvider)) { - var policy = hasNewUserPolicy.GetNewUserPolicy(); - UpdateUserPolicy(user, policy, true); + var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy; + if (hasNewUserPolicy != null) + { + var policy = hasNewUserPolicy.GetNewUserPolicy(); + UpdateUserPolicy(user, policy, true); + } } } } @@ -400,7 +407,10 @@ namespace Emby.Server.Implementations.Library if (providers.Length == 0) { - providers = new IAuthenticationProvider[] { _defaultAuthenticationProvider }; + // this function used to assign any user without an auth provider to the default. + // we're going to have it use a new function now. + _logger.LogWarning($"The user {user.Name} was found but no Authentication Provider with ID: {user.Policy.AuthenticationProviderId} was found. Assigning user to InvalidAuthProvider temporarily"); + providers = new IAuthenticationProvider[] { _invalidAuthProvider }; } return providers; -- cgit v1.2.3 From 74ef3898798033a7cad987c4a869e7e72f57b229 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 9 Jun 2019 11:07:35 -0400 Subject: Add nicer log message and comment --- Emby.Server.Implementations/Library/UserManager.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index ca43f7aaa..83584acf3 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -407,9 +407,8 @@ namespace Emby.Server.Implementations.Library if (providers.Length == 0) { - // this function used to assign any user without an auth provider to the default. - // we're going to have it use a new function now. - _logger.LogWarning($"The user {user.Name} was found but no Authentication Provider with ID: {user.Policy.AuthenticationProviderId} was found. Assigning user to InvalidAuthProvider temporarily"); + // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found + _logger.LogWarning("User {0} was found with invalid/missing Authentication Provider {1}. Assigning user to InvalidAuthProvider until this is corrected", user.Name, user.Policy.AuthenticationProviderId); providers = new IAuthenticationProvider[] { _invalidAuthProvider }; } -- cgit v1.2.3 From b70083f3b370055b2942e450291ce42345732cb7 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 9 Jun 2019 13:41:14 -0400 Subject: Apply suggestions from code review Co-Authored-By: Claus Vium Co-Authored-By: Bond-009 --- Emby.Server.Implementations/Library/InvalidAuthProvider.cs | 8 ++++---- Emby.Server.Implementations/Library/UserManager.cs | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/InvalidAuthProvider.cs b/Emby.Server.Implementations/Library/InvalidAuthProvider.cs index ee2569562..133864708 100644 --- a/Emby.Server.Implementations/Library/InvalidAuthProvider.cs +++ b/Emby.Server.Implementations/Library/InvalidAuthProvider.cs @@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Library { public class InvalidAuthProvider : IAuthenticationProvider { - public string Name => "InvalidorMissingAuthenticationProvider"; + public string Name => "InvalidOrMissingAuthenticationProvider"; public bool IsEnabled => true; @@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Library public Task ChangePassword(User user, string newPassword) { - return Task.FromResult(true); + return Task.CompletedTask; } public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) @@ -35,12 +35,12 @@ namespace Emby.Server.Implementations.Library public string GetPasswordHash(User user) { - return ""; + return string.Empty; } public string GetEasyPasswordHash(User user) { - return ""; + return string.Empty; } } } diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 83584acf3..04abfc315 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -313,8 +313,7 @@ namespace Emby.Server.Implementations.Library if (authenticationProvider.GetType() != typeof(InvalidAuthProvider)) { - var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy; - if (hasNewUserPolicy != null) + if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy) { var policy = hasNewUserPolicy.GetNewUserPolicy(); UpdateUserPolicy(user, policy, true); @@ -408,7 +407,7 @@ namespace Emby.Server.Implementations.Library if (providers.Length == 0) { // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found - _logger.LogWarning("User {0} was found with invalid/missing Authentication Provider {1}. Assigning user to InvalidAuthProvider until this is corrected", user.Name, user.Policy.AuthenticationProviderId); + _logger.LogWarning("User {UserName} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected", user.Name, user.Policy.AuthenticationProviderId); providers = new IAuthenticationProvider[] { _invalidAuthProvider }; } -- cgit v1.2.3 From c230d49d7c37d4fbe77676b835c3afd6c8cb56e7 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 9 Jun 2019 13:46:53 -0400 Subject: Don't set a default reset provider --- Emby.Server.Implementations/Library/UserManager.cs | 5 ----- 1 file changed, 5 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 04abfc315..4233ea8f4 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -425,11 +425,6 @@ namespace Emby.Server.Implementations.Library providers = providers.Where(i => string.Equals(passwordResetProviderId, GetPasswordResetProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray(); } - if (providers.Length == 0) - { - providers = new IPasswordResetProvider[] { _defaultPasswordResetProvider }; - } - return providers; } -- cgit v1.2.3 From 4b8f735cb89901bd1004d590f4f2820c23e2493c Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 9 Jun 2019 13:57:49 -0400 Subject: Remove superfluous conditional This wasn't needed to prevent updating the policy on-disk from my tests and can be removed as suggested by @Bond-009 --- Emby.Server.Implementations/Library/UserManager.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 4233ea8f4..16becbd52 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -311,13 +311,10 @@ namespace Emby.Server.Implementations.Library user = Users .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); - if (authenticationProvider.GetType() != typeof(InvalidAuthProvider)) + if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy) { - if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy) - { - var policy = hasNewUserPolicy.GetNewUserPolicy(); - UpdateUserPolicy(user, policy, true); - } + var policy = hasNewUserPolicy.GetNewUserPolicy(); + UpdateUserPolicy(user, policy, true); } } } -- cgit v1.2.3 From 2946ae10092cddadade4c84cfa000129bf117e03 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 9 Jun 2019 15:27:38 -0400 Subject: Revert "Don't set a default reset provider" This reverts commit c230d49d7c37d4fbe77676b835c3afd6c8cb56e7. This reenables an edge case where an admin might want to reset, with the default auth provider, the password of an externally-provided user so they could "unlock" the account while it was failing. There might be minor security implications to this, but the malicious actor would need FS access to do it (as they would with any password resets) so it's probably best to keep it as-is. Removing this in the first place was due to a misunderstanding anyways so no harm. --- Emby.Server.Implementations/Library/UserManager.cs | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 16becbd52..ff375e590 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -422,6 +422,11 @@ namespace Emby.Server.Implementations.Library providers = providers.Where(i => string.Equals(passwordResetProviderId, GetPasswordResetProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray(); } + if (providers.Length == 0) + { + providers = new IPasswordResetProvider[] { _defaultPasswordResetProvider }; + } + return providers; } -- cgit v1.2.3 From d961278b3dcab57910b260115cd45d9831e6443e Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 3 Apr 2019 18:15:04 +0200 Subject: Reduce amount of raw sql --- .../Data/SqliteUserRepository.cs | 23 ++++++------ Emby.Server.Implementations/Library/UserManager.cs | 42 +++++++++++----------- 2 files changed, 34 insertions(+), 31 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs index cd364e7f4..de2354eef 100644 --- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs @@ -75,10 +75,8 @@ namespace Emby.Server.Implementations.Data private void RemoveEmptyPasswordHashes(ManagedConnection connection) { - foreach (var row in connection.Query("select id,guid,data from LocalUsersv2")) + foreach (var user in RetrieveAllUsers(connection)) { - var user = GetUser(row); - // If the user password is the sha1 hash of the empty string, remove it if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal) && !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)) @@ -198,17 +196,22 @@ namespace Emby.Server.Implementations.Data /// IEnumerable{User}. public List RetrieveAllUsers() { - var list = new List(); - using (var connection = GetConnection(true)) { - foreach (var row in connection.Query("select id,guid,data from LocalUsersv2")) - { - list.Add(GetUser(row)); - } + return new List(RetrieveAllUsers(connection)); } + } - return list; + /// + /// Retrieve all users from the database + /// + /// IEnumerable{User}. + private IEnumerable RetrieveAllUsers(ManagedConnection connection) + { + foreach (var row in connection.Query("select id,guid,data from LocalUsersv2")) + { + yield return GetUser(row); + } } /// diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index ff375e590..1701ced42 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -222,9 +222,8 @@ namespace Emby.Server.Implementations.Library public void Initialize() { - _users = LoadUsers(); - - var users = Users.ToList(); + var users = LoadUsers(); + _users = users.ToArray(); // If there are no local users with admin rights, make them all admins if (!users.Any(i => i.Policy.IsAdministrator)) @@ -555,35 +554,36 @@ namespace Emby.Server.Implementations.Library /// Loads the users from the repository /// /// IEnumerable{User}. - private User[] LoadUsers() + private List LoadUsers() { var users = UserRepository.RetrieveAllUsers(); // There always has to be at least one user. - if (users.Count == 0) + if (users.Count != 0) { - var defaultName = Environment.UserName; - if (string.IsNullOrWhiteSpace(defaultName)) - { - defaultName = "MyJellyfinUser"; - } - var name = MakeValidUsername(defaultName); + return users; + } - var user = InstantiateNewUser(name); + var defaultName = Environment.UserName; + if (string.IsNullOrWhiteSpace(defaultName)) + { + defaultName = "MyJellyfinUser"; + } - user.DateLastSaved = DateTime.UtcNow; + var name = MakeValidUsername(defaultName); - UserRepository.CreateUser(user); + var user = InstantiateNewUser(name); + + user.DateLastSaved = DateTime.UtcNow; - users.Add(user); + UserRepository.CreateUser(user); - user.Policy.IsAdministrator = true; - user.Policy.EnableContentDeletion = true; - user.Policy.EnableRemoteControlOfOtherUsers = true; - UpdateUserPolicy(user, user.Policy, false); - } + user.Policy.IsAdministrator = true; + user.Policy.EnableContentDeletion = true; + user.Policy.EnableRemoteControlOfOtherUsers = true; + UpdateUserPolicy(user, user.Policy, false); - return users.ToArray(); + return new List { user }; } public UserDto GetUserDto(User user, string remoteEndPoint = null) -- cgit v1.2.3 From 0f897589ed6349bb3c88919b06861daa80aec1be Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 21 May 2019 19:28:34 +0200 Subject: Streamline authentication proccess --- .../Cryptography/CryptographyProvider.cs | 86 ++++--- .../Library/DefaultAuthenticationProvider.cs | 115 +++++---- .../Library/DefaultPasswordResetProvider.cs | 257 ++++++++++----------- .../Library/InvalidAuthProvider.cs | 11 +- Emby.Server.Implementations/Library/UserManager.cs | 49 ++-- MediaBrowser.Api/LiveTv/LiveTvService.cs | 19 -- .../Authentication/AuthenticationException.cs | 28 +++ .../Authentication/IAuthenticationProvider.cs | 3 +- .../Authentication/IPasswordResetProvider.cs | 1 + MediaBrowser.Model/Cryptography/ICryptoProvider.cs | 6 +- MediaBrowser.Model/Cryptography/PasswordHash.cs | 152 ++++++------ 11 files changed, 365 insertions(+), 362 deletions(-) create mode 100644 MediaBrowser.Controller/Authentication/AuthenticationException.cs (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 6d7193ce2..f726dae2e 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -8,7 +8,7 @@ using MediaBrowser.Model.Cryptography; namespace Emby.Server.Implementations.Cryptography { - public class CryptographyProvider : ICryptoProvider + public class CryptographyProvider : ICryptoProvider, IDisposable { private static readonly HashSet _supportedHashMethods = new HashSet() { @@ -28,26 +28,28 @@ namespace Emby.Server.Implementations.Cryptography "System.Security.Cryptography.SHA512" }; - public string DefaultHashMethod => "PBKDF2"; - private RandomNumberGenerator _randomNumberGenerator; private const int _defaultIterations = 1000; + private bool _disposed = false; + public CryptographyProvider() { - //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 + // 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 _randomNumberGenerator = RandomNumberGenerator.Create(); } + public string DefaultHashMethod => "PBKDF2"; + + [Obsolete("Use System.Security.Cryptography.MD5 directly")] public Guid GetMD5(string str) - { - return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str))); - } + => new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str))); + [Obsolete("Use System.Security.Cryptography.SHA1 directly")] public byte[] ComputeSHA1(byte[] bytes) { using (var provider = SHA1.Create()) @@ -56,6 +58,7 @@ namespace Emby.Server.Implementations.Cryptography } } + [Obsolete("Use System.Security.Cryptography.MD5 directly")] public byte[] ComputeMD5(Stream str) { using (var provider = MD5.Create()) @@ -64,6 +67,7 @@ namespace Emby.Server.Implementations.Cryptography } } + [Obsolete("Use System.Security.Cryptography.MD5 directly")] public byte[] ComputeMD5(byte[] bytes) { using (var provider = MD5.Create()) @@ -73,9 +77,7 @@ namespace Emby.Server.Implementations.Cryptography } public IEnumerable GetSupportedHashMethods() - { - return _supportedHashMethods; - } + => _supportedHashMethods; private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations) { @@ -93,14 +95,10 @@ namespace Emby.Server.Implementations.Cryptography } public byte[] ComputeHash(string hashMethod, byte[] bytes) - { - return ComputeHash(hashMethod, bytes, Array.Empty()); - } + => ComputeHash(hashMethod, bytes, Array.Empty()); public byte[] ComputeHashWithDefaultMethod(byte[] bytes) - { - return ComputeHash(DefaultHashMethod, bytes); - } + => ComputeHash(DefaultHashMethod, bytes); public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt) { @@ -125,37 +123,27 @@ namespace Emby.Server.Implementations.Cryptography } } } - else - { - throw new CryptographicException($"Requested hash method is not supported: {hashMethod}"); - } + + throw new CryptographicException($"Requested hash method is not supported: {hashMethod}"); + } public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt) - { - return PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations); - } + => PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations); public byte[] ComputeHash(PasswordHash hash) { int iterations = _defaultIterations; if (!hash.Parameters.ContainsKey("iterations")) { - hash.Parameters.Add("iterations", _defaultIterations.ToString(CultureInfo.InvariantCulture)); + hash.Parameters.Add("iterations", iterations.ToString(CultureInfo.InvariantCulture)); } - else + else if (!int.TryParse(hash.Parameters["iterations"], out iterations)) { - try - { - iterations = int.Parse(hash.Parameters["iterations"]); - } - catch (Exception e) - { - throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}", e); - } + throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}"); } - return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes, iterations); + return PBKDF2(hash.Id, hash.Hash, hash.Salt, iterations); } public byte[] GenerateSalt() @@ -164,5 +152,29 @@ namespace Emby.Server.Implementations.Cryptography _randomNumberGenerator.GetBytes(salt); return salt; } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + _randomNumberGenerator.Dispose(); + } + + _randomNumberGenerator = null; + + _disposed = true; + } } } diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index fe09b07ff..b07244fda 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -11,9 +11,9 @@ namespace Emby.Server.Implementations.Library public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser { private readonly ICryptoProvider _cryptographyProvider; - public DefaultAuthenticationProvider(ICryptoProvider crypto) + public DefaultAuthenticationProvider(ICryptoProvider cryptographyProvider) { - _cryptographyProvider = crypto; + _cryptographyProvider = cryptographyProvider; } public string Name => "Default"; @@ -28,17 +28,17 @@ namespace Emby.Server.Implementations.Library throw new NotImplementedException(); } - // This is the verson that we need to use for local users. Because reasons. + // This is the version that we need to use for local users. Because reasons. public Task Authenticate(string username, string password, User resolvedUser) { bool success = false; if (resolvedUser == null) { - throw new Exception("Invalid username or password"); + throw new ArgumentNullException(nameof(resolvedUser)); } // As long as jellyfin supports passwordless users, we need this little block here to accomodate - if (IsPasswordEmpty(resolvedUser, password)) + if (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password)) { return Task.FromResult(new ProviderAuthenticationResult { @@ -50,37 +50,24 @@ namespace Emby.Server.Implementations.Library byte[] passwordbytes = Encoding.UTF8.GetBytes(password); PasswordHash readyHash = new PasswordHash(resolvedUser.Password); - byte[] calculatedHash; - string calculatedHashString; - if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id) || _cryptographyProvider.DefaultHashMethod == readyHash.Id) + if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id) + || _cryptographyProvider.DefaultHashMethod == readyHash.Id) { - if (string.IsNullOrEmpty(readyHash.Salt)) - { - calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes); - calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty); - } - else - { - calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes); - calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty); - } + byte[] calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.Salt); - if (calculatedHashString == readyHash.Hash) + if (calculatedHash.SequenceEqual(readyHash.Hash)) { success = true; - // throw new Exception("Invalid username or password"); } } else { - throw new Exception(string.Format($"Requested crypto method not available in provider: {readyHash.Id}")); + throw new AuthenticationException($"Requested crypto method not available in provider: {readyHash.Id}"); } - // var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); - if (!success) { - throw new Exception("Invalid username or password"); + throw new AuthenticationException("Invalid username or password"); } return Task.FromResult(new ProviderAuthenticationResult @@ -98,29 +85,22 @@ namespace Emby.Server.Implementations.Library return; } - if (!user.Password.Contains("$")) + if (user.Password.IndexOf('$') == -1) { string hash = user.Password; user.Password = string.Format("$SHA1${0}", hash); } - if (user.EasyPassword != null && !user.EasyPassword.Contains("$")) + if (user.EasyPassword != null + && user.EasyPassword.IndexOf('$') == -1) { string hash = user.EasyPassword; user.EasyPassword = string.Format("$SHA1${0}", hash); } } - public Task HasPassword(User user) - { - var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user)); - return Task.FromResult(hasConfiguredPassword); - } - - private bool IsPasswordEmpty(User user, string password) - { - return (string.IsNullOrEmpty(user.Password) && string.IsNullOrEmpty(password)); - } + public bool HasPassword(User user) + => !string.IsNullOrEmpty(user.Password); public Task ChangePassword(User user, string newPassword) { @@ -129,30 +109,24 @@ namespace Emby.Server.Implementations.Library if (string.IsNullOrEmpty(user.Password)) { PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider); - newPasswordHash.SaltBytes = _cryptographyProvider.GenerateSalt(); - newPasswordHash.Salt = PasswordHash.ConvertToByteString(newPasswordHash.SaltBytes); + newPasswordHash.Salt = _cryptographyProvider.GenerateSalt(); newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod; - newPasswordHash.Hash = GetHashedStringChangeAuth(newPassword, newPasswordHash); + newPasswordHash.Hash = GetHashedChangeAuth(newPassword, newPasswordHash); user.Password = newPasswordHash.ToString(); return Task.CompletedTask; } PasswordHash passwordHash = new PasswordHash(user.Password); - if (passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt)) + if (passwordHash.Id == "SHA1" + && passwordHash.Salt.Length == 0) { - passwordHash.SaltBytes = _cryptographyProvider.GenerateSalt(); - passwordHash.Salt = PasswordHash.ConvertToByteString(passwordHash.SaltBytes); + passwordHash.Salt = _cryptographyProvider.GenerateSalt(); passwordHash.Id = _cryptographyProvider.DefaultHashMethod; - passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash); + passwordHash.Hash = GetHashedChangeAuth(newPassword, passwordHash); } else if (newPassword != null) { - passwordHash.Hash = GetHashedString(user, newPassword); - } - - if (string.IsNullOrWhiteSpace(passwordHash.Hash)) - { - throw new ArgumentNullException(nameof(passwordHash.Hash)); + passwordHash.Hash = GetHashed(user, newPassword); } user.Password = passwordHash.ToString(); @@ -160,11 +134,6 @@ namespace Emby.Server.Implementations.Library return Task.CompletedTask; } - public string GetPasswordHash(User user) - { - return user.Password; - } - public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) { ConvertPasswordFormat(user); @@ -190,13 +159,13 @@ namespace Emby.Server.Implementations.Library return string.IsNullOrEmpty(user.EasyPassword) ? null - : (new PasswordHash(user.EasyPassword)).Hash; + : PasswordHash.ConvertToByteString(new PasswordHash(user.EasyPassword).Hash); } - public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash) + internal byte[] GetHashedChangeAuth(string newPassword, PasswordHash passwordHash) { - passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword); - return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash)); + passwordHash.Hash = Encoding.UTF8.GetBytes(newPassword); + return _cryptographyProvider.ComputeHash(passwordHash); } /// @@ -215,10 +184,10 @@ namespace Emby.Server.Implementations.Library passwordHash = new PasswordHash(user.Password); } - if (passwordHash.SaltBytes != null) + if (passwordHash.Salt != null) { // the password is modern format with PBKDF and we should take advantage of that - passwordHash.HashBytes = Encoding.UTF8.GetBytes(str); + passwordHash.Hash = Encoding.UTF8.GetBytes(str); return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash)); } else @@ -227,5 +196,31 @@ namespace Emby.Server.Implementations.Library return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str))); } } + + public byte[] GetHashed(User user, string str) + { + PasswordHash passwordHash; + if (string.IsNullOrEmpty(user.Password)) + { + passwordHash = new PasswordHash(_cryptographyProvider); + } + else + { + ConvertPasswordFormat(user); + passwordHash = new PasswordHash(user.Password); + } + + if (passwordHash.Salt != null) + { + // the password is modern format with PBKDF and we should take advantage of that + passwordHash.Hash = Encoding.UTF8.GetBytes(str); + return _cryptographyProvider.ComputeHash(passwordHash); + } + else + { + // the password has no salt and should be called with the older method for safety + return _cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str)); + } + } } } diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs index e218749d9..c7044820c 100644 --- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs @@ -1,132 +1,125 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; - -namespace Emby.Server.Implementations.Library -{ - public class DefaultPasswordResetProvider : IPasswordResetProvider - { - public string Name => "Default Password Reset Provider"; - - public bool IsEnabled => true; - - private readonly string _passwordResetFileBase; - private readonly string _passwordResetFileBaseDir; - private readonly string _passwordResetFileBaseName = "passwordreset"; - - private readonly IJsonSerializer _jsonSerializer; - private readonly IUserManager _userManager; - private readonly ICryptoProvider _crypto; - - public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager, ICryptoProvider cryptoProvider) - { - _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; - _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName); - _jsonSerializer = jsonSerializer; - _userManager = userManager; - _crypto = cryptoProvider; - } - - public async Task RedeemPasswordResetPin(string pin) - { - SerializablePasswordReset spr; - HashSet usersreset = new HashSet(); - foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*")) - { - using (var str = File.OpenRead(resetfile)) - { - spr = await _jsonSerializer.DeserializeFromStreamAsync(str).ConfigureAwait(false); - } - - if (spr.ExpirationDate < DateTime.Now) - { - File.Delete(resetfile); - } - else if (spr.Pin.Replace("-", "").Equals(pin.Replace("-", ""), StringComparison.InvariantCultureIgnoreCase)) - { - var resetUser = _userManager.GetUserByName(spr.UserName); - if (resetUser == null) - { - throw new Exception($"User with a username of {spr.UserName} not found"); - } - - await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); - usersreset.Add(resetUser.Name); - File.Delete(resetfile); - } - } - - if (usersreset.Count < 1) - { - throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}"); - } - else - { - return new PinRedeemResult - { - Success = true, - UsersReset = usersreset.ToArray() - }; - } - } - - public async Task StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork) - { - string pin = string.Empty; - using (var cryptoRandom = System.Security.Cryptography.RandomNumberGenerator.Create()) - { - byte[] bytes = new byte[4]; - cryptoRandom.GetBytes(bytes); - pin = BitConverter.ToString(bytes); - } - - DateTime expireTime = DateTime.Now.AddMinutes(30); - string filePath = _passwordResetFileBase + user.InternalId + ".json"; - SerializablePasswordReset spr = new SerializablePasswordReset - { - ExpirationDate = expireTime, - Pin = pin, - PinFile = filePath, - UserName = user.Name - }; - - try - { - using (FileStream fileStream = File.OpenWrite(filePath)) - { - _jsonSerializer.SerializeToStream(spr, fileStream); - await fileStream.FlushAsync().ConfigureAwait(false); - } - } - catch (Exception e) - { - throw new Exception($"Error serializing or writing password reset for {user.Name} to location: {filePath}", e); - } - - return new ForgotPasswordResult - { - Action = ForgotPasswordAction.PinCode, - PinExpirationDate = expireTime, - PinFile = filePath - }; - } - - private class SerializablePasswordReset : PasswordPinCreationResult - { - public string Pin { get; set; } - - public string UserName { get; set; } - } - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Users; + +namespace Emby.Server.Implementations.Library +{ + public class DefaultPasswordResetProvider : IPasswordResetProvider + { + public string Name => "Default Password Reset Provider"; + + public bool IsEnabled => true; + + private readonly string _passwordResetFileBase; + private readonly string _passwordResetFileBaseDir; + private readonly string _passwordResetFileBaseName = "passwordreset"; + + private readonly IJsonSerializer _jsonSerializer; + private readonly IUserManager _userManager; + private readonly ICryptoProvider _crypto; + + public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager, ICryptoProvider cryptoProvider) + { + _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; + _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName); + _jsonSerializer = jsonSerializer; + _userManager = userManager; + _crypto = cryptoProvider; + } + + public async Task RedeemPasswordResetPin(string pin) + { + SerializablePasswordReset spr; + HashSet usersreset = new HashSet(); + foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*")) + { + using (var str = File.OpenRead(resetfile)) + { + spr = await _jsonSerializer.DeserializeFromStreamAsync(str).ConfigureAwait(false); + } + + if (spr.ExpirationDate < DateTime.Now) + { + File.Delete(resetfile); + } + else if (spr.Pin.Replace("-", "").Equals(pin.Replace("-", ""), StringComparison.InvariantCultureIgnoreCase)) + { + var resetUser = _userManager.GetUserByName(spr.UserName); + if (resetUser == null) + { + throw new Exception($"User with a username of {spr.UserName} not found"); + } + + await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); + usersreset.Add(resetUser.Name); + File.Delete(resetfile); + } + } + + if (usersreset.Count < 1) + { + throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}"); + } + else + { + return new PinRedeemResult + { + Success = true, + UsersReset = usersreset.ToArray() + }; + } + } + + public async Task StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork) + { + string pin = string.Empty; + using (var cryptoRandom = System.Security.Cryptography.RandomNumberGenerator.Create()) + { + byte[] bytes = new byte[4]; + cryptoRandom.GetBytes(bytes); + pin = BitConverter.ToString(bytes); + } + + DateTime expireTime = DateTime.Now.AddMinutes(30); + string filePath = _passwordResetFileBase + user.InternalId + ".json"; + SerializablePasswordReset spr = new SerializablePasswordReset + { + ExpirationDate = expireTime, + Pin = pin, + PinFile = filePath, + UserName = user.Name + }; + + using (FileStream fileStream = File.OpenWrite(filePath)) + { + _jsonSerializer.SerializeToStream(spr, fileStream); + await fileStream.FlushAsync().ConfigureAwait(false); + } + + return new ForgotPasswordResult + { + Action = ForgotPasswordAction.PinCode, + PinExpirationDate = expireTime, + PinFile = filePath + }; + } + + private class SerializablePasswordReset : PasswordPinCreationResult + { + public string Pin { get; set; } + + public string UserName { get; set; } + } + } +} diff --git a/Emby.Server.Implementations/Library/InvalidAuthProvider.cs b/Emby.Server.Implementations/Library/InvalidAuthProvider.cs index 25d233137..6956369dc 100644 --- a/Emby.Server.Implementations/Library/InvalidAuthProvider.cs +++ b/Emby.Server.Implementations/Library/InvalidAuthProvider.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Entities; @@ -16,12 +13,12 @@ namespace Emby.Server.Implementations.Library public Task Authenticate(string username, string password) { - throw new SecurityException("User Account cannot login with this provider. The Normal provider for this user cannot be found"); + throw new AuthenticationException("User Account cannot login with this provider. The Normal provider for this user cannot be found"); } - public Task HasPassword(User user) + public bool HasPassword(User user) { - return Task.FromResult(true); + return true; } public Task ChangePassword(User user, string newPassword) @@ -31,7 +28,7 @@ namespace Emby.Server.Implementations.Library public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) { - // Nothing here + // Nothing here } public string GetPasswordHash(User user) diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 1701ced42..c8c8a108d 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -266,6 +266,7 @@ namespace Emby.Server.Implementations.Library builder.Append(c); } } + return builder.ToString(); } @@ -286,17 +287,17 @@ namespace Emby.Server.Implementations.Library if (user != null) { var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false); - authenticationProvider = authResult.Item1; - updatedUsername = authResult.Item2; - success = authResult.Item3; + authenticationProvider = authResult.authenticationProvider; + updatedUsername = authResult.username; + success = authResult.success; } else { // user is null var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); - authenticationProvider = authResult.Item1; - updatedUsername = authResult.Item2; - success = authResult.Item3; + authenticationProvider = authResult.authenticationProvider; + updatedUsername = authResult.username; + success = authResult.success; if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) { @@ -331,22 +332,25 @@ namespace Emby.Server.Implementations.Library if (user == null) { - throw new SecurityException("Invalid username or password entered."); + throw new AuthenticationException("Invalid username or password entered."); } if (user.Policy.IsDisabled) { - throw new SecurityException(string.Format("The {0} account is currently disabled. Please consult with your administrator.", user.Name)); + throw new AuthenticationException(string.Format( + CultureInfo.InvariantCulture, + "The {0} account is currently disabled. Please consult with your administrator.", + user.Name)); } if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint)) { - throw new SecurityException("Forbidden."); + throw new AuthenticationException("Forbidden."); } if (!user.IsParentalScheduleAllowed()) { - throw new SecurityException("User is not allowed access at this time."); + throw new AuthenticationException("User is not allowed access at this time."); } // Update LastActivityDate and LastLoginDate, then save @@ -357,6 +361,7 @@ namespace Emby.Server.Implementations.Library user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; UpdateUser(user); } + UpdateInvalidLoginAttemptCount(user, 0); } else @@ -429,7 +434,7 @@ namespace Emby.Server.Implementations.Library return providers; } - private async Task> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) + private async Task<(string username, bool success)> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) { try { @@ -444,23 +449,23 @@ namespace Emby.Server.Implementations.Library authenticationResult = await provider.Authenticate(username, password).ConfigureAwait(false); } - if(authenticationResult.Username != username) + if (authenticationResult.Username != username) { _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username); username = authenticationResult.Username; } - return new Tuple(username, true); + return (username, true); } - catch (Exception ex) + catch (AuthenticationException ex) { - _logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name); + _logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name); - return new Tuple(username, false); + return (username, false); } } - private async Task> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) + private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) { string updatedUsername = null; bool success = false; @@ -475,15 +480,15 @@ namespace Emby.Server.Implementations.Library if (password == null) { // legacy - success = string.Equals(GetAuthenticationProvider(user).GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + success = string.Equals(user.Password, hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); } else { foreach (var provider in GetAuthenticationProviders(user)) { var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); - updatedUsername = providerAuthResult.Item1; - success = providerAuthResult.Item2; + updatedUsername = providerAuthResult.username; + success = providerAuthResult.success; if (success) { @@ -510,7 +515,7 @@ namespace Emby.Server.Implementations.Library } } - return new Tuple(authenticationProvider, username, success); + return (authenticationProvider, username, success); } private void UpdateInvalidLoginAttemptCount(User user, int newValue) @@ -593,7 +598,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(user)); } - bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; + bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user); bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetAuthenticationProvider(user).GetEasyPasswordHash(user)); bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index e41ad540a..8a4d6e216 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -599,7 +599,6 @@ namespace MediaBrowser.Api.LiveTv { public bool ValidateLogin { get; set; } public bool ValidateListings { get; set; } - public string Pw { get; set; } } [Route("/LiveTv/ListingProviders", "DELETE", Summary = "Deletes a listing provider")] @@ -867,28 +866,10 @@ namespace MediaBrowser.Api.LiveTv public async Task Post(AddListingProvider request) { - if (request.Pw != null) - { - request.Password = GetHashedString(request.Pw); - } - - request.Pw = null; - var result = await _liveTvManager.SaveListingProvider(request, request.ValidateLogin, request.ValidateListings).ConfigureAwait(false); return ToOptimizedResult(result); } - /// - /// Gets the hashed string. - /// - private string GetHashedString(string str) - { - // legacy - return BitConverter.ToString( - _cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))) - .Replace("-", string.Empty).ToLowerInvariant(); - } - public void Delete(DeleteListingProvider request) { _liveTvManager.DeleteListingsProvider(request.Id); diff --git a/MediaBrowser.Controller/Authentication/AuthenticationException.cs b/MediaBrowser.Controller/Authentication/AuthenticationException.cs new file mode 100644 index 000000000..045cbcdae --- /dev/null +++ b/MediaBrowser.Controller/Authentication/AuthenticationException.cs @@ -0,0 +1,28 @@ +using System; +namespace MediaBrowser.Controller.Authentication +{ + /// + /// The exception that is thrown when an attempt to authenticate fails. + /// + public class AuthenticationException : Exception + { + /// + public AuthenticationException() : base() + { + + } + + /// + public AuthenticationException(string message) : base(message) + { + + } + + /// + public AuthenticationException(string message, Exception innerException) + : base(message, innerException) + { + + } + } +} diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs index 2cf531eed..f5571065f 100644 --- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs +++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs @@ -9,10 +9,9 @@ namespace MediaBrowser.Controller.Authentication string Name { get; } bool IsEnabled { get; } Task Authenticate(string username, string password); - Task HasPassword(User user); + bool HasPassword(User user); Task ChangePassword(User user, string newPassword); void ChangeEasyPassword(User user, string newPassword, string newPasswordHash); - string GetPasswordHash(User user); string GetEasyPasswordHash(User user); } diff --git a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs index 9e5cd8816..2639960e7 100644 --- a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs +++ b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs @@ -12,6 +12,7 @@ namespace MediaBrowser.Controller.Authentication Task StartForgotPasswordProcess(User user, bool isInNetwork); Task RedeemPasswordResetPin(string pin); } + public class PasswordPinCreationResult { public string PinFile { get; set; } diff --git a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs index 5988112c2..9e85beb43 100644 --- a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs +++ b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs @@ -6,9 +6,14 @@ namespace MediaBrowser.Model.Cryptography { public interface ICryptoProvider { + string DefaultHashMethod { get; } + [Obsolete("Use System.Security.Cryptography.MD5 directly")] Guid GetMD5(string str); + [Obsolete("Use System.Security.Cryptography.MD5 directly")] byte[] ComputeMD5(Stream str); + [Obsolete("Use System.Security.Cryptography.MD5 directly")] byte[] ComputeMD5(byte[] bytes); + [Obsolete("Use System.Security.Cryptography.SHA1 directly")] byte[] ComputeSHA1(byte[] bytes); IEnumerable GetSupportedHashMethods(); byte[] ComputeHash(string HashMethod, byte[] bytes); @@ -17,6 +22,5 @@ namespace MediaBrowser.Model.Cryptography byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt); byte[] ComputeHash(PasswordHash hash); byte[] GenerateSalt(); - string DefaultHashMethod { get; } } } diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs index f15b27d32..df32fdb00 100644 --- a/MediaBrowser.Model/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text; namespace MediaBrowser.Model.Cryptography @@ -16,86 +17,71 @@ namespace MediaBrowser.Model.Cryptography private Dictionary _parameters = new Dictionary(); - private string _salt; + private byte[] _salt; - private byte[] _saltBytes; - - private string _hash; - - private byte[] _hashBytes; - - public string Id { get => _id; set => _id = value; } - - public Dictionary Parameters { get => _parameters; set => _parameters = value; } - - public string Salt { get => _salt; set => _salt = value; } - - public byte[] SaltBytes { get => _saltBytes; set => _saltBytes = value; } - - public string Hash { get => _hash; set => _hash = value; } - - public byte[] HashBytes { get => _hashBytes; set => _hashBytes = value; } + private byte[] _hash; public PasswordHash(string storageString) { string[] splitted = storageString.Split('$'); - _id = splitted[1]; - if (splitted[2].Contains("=")) + // The string should at least contain the hash function and the hash itself + if (splitted.Length < 3) + { + throw new ArgumentException("String doesn't contain enough segments", nameof(storageString)); + } + + // Start at 1, the first index shouldn't contain any data + int index = 1; + + // Name of the hash function + _id = splitted[index++]; + + // Optional parameters + if (splitted[index].IndexOf('=') != -1) { - foreach (string paramset in (splitted[2].Split(','))) + foreach (string paramset in splitted[index++].Split(',')) { - if (!string.IsNullOrEmpty(paramset)) + if (string.IsNullOrEmpty(paramset)) { - string[] fields = paramset.Split('='); - if (fields.Length == 2) - { - _parameters.Add(fields[0], fields[1]); - } - else - { - throw new Exception($"Malformed parameter in password hash string {paramset}"); - } + continue; } + + string[] fields = paramset.Split('='); + if (fields.Length != 2) + { + throw new InvalidDataException($"Malformed parameter in password hash string {paramset}"); + } + + _parameters.Add(fields[0], fields[1]); } - if (splitted.Length == 5) - { - _salt = splitted[3]; - _saltBytes = ConvertFromByteString(_salt); - _hash = splitted[4]; - _hashBytes = ConvertFromByteString(_hash); - } - else - { - _salt = string.Empty; - _hash = splitted[3]; - _hashBytes = ConvertFromByteString(_hash); - } + } + + // Check if the string also contains a salt + if (splitted.Length - index == 2) + { + _salt = ConvertFromByteString(splitted[index++]); + _hash = ConvertFromByteString(splitted[index++]); } else { - if (splitted.Length == 4) - { - _salt = splitted[2]; - _saltBytes = ConvertFromByteString(_salt); - _hash = splitted[3]; - _hashBytes = ConvertFromByteString(_hash); - } - else - { - _salt = string.Empty; - _hash = splitted[2]; - _hashBytes = ConvertFromByteString(_hash); - } - + _salt = Array.Empty(); + _hash = ConvertFromByteString(splitted[index++]); } - } + public string Id { get => _id; set => _id = value; } + + public Dictionary Parameters { get => _parameters; set => _parameters = value; } + + public byte[] Salt { get => _salt; set => _salt = value; } + + public byte[] Hash { get => _hash; set => _hash = value; } + public PasswordHash(ICryptoProvider cryptoProvider) { _id = cryptoProvider.DefaultHashMethod; - _saltBytes = cryptoProvider.GenerateSalt(); - _salt = ConvertToByteString(SaltBytes); + _salt = cryptoProvider.GenerateSalt(); + _hash = Array.Empty(); } public static byte[] ConvertFromByteString(string byteString) @@ -111,43 +97,45 @@ namespace MediaBrowser.Model.Cryptography } public static string ConvertToByteString(byte[] bytes) - { - return BitConverter.ToString(bytes).Replace("-", ""); - } + => BitConverter.ToString(bytes).Replace("-", string.Empty); - private string SerializeParameters() + private void SerializeParameters(StringBuilder stringBuilder) { - string returnString = string.Empty; - foreach (var KVP in _parameters) + if (_parameters.Count == 0) { - returnString += $",{KVP.Key}={KVP.Value}"; + return; } - if ((!string.IsNullOrEmpty(returnString)) && returnString[0] == ',') + stringBuilder.Append('$'); + foreach (var pair in _parameters) { - returnString = returnString.Remove(0, 1); + stringBuilder.Append(pair.Key); + stringBuilder.Append('='); + stringBuilder.Append(pair.Value); + stringBuilder.Append(','); } - return returnString; + // Remove last ',' + stringBuilder.Length -= 1; } public override string ToString() { - string outString = "$" + _id; - string paramstring = SerializeParameters(); - if (!string.IsNullOrEmpty(paramstring)) - { - outString += $"${paramstring}"; - } + var str = new StringBuilder(); + str.Append('$'); + str.Append(_id); + SerializeParameters(str); - if (!string.IsNullOrEmpty(_salt)) + if (_salt.Length == 0) { - outString += $"${_salt}"; + str.Append('$'); + str.Append(ConvertToByteString(_salt)); } - outString += $"${_hash}"; - return outString; + str.Append('$'); + str.Append(ConvertToByteString(_hash)); + + return str.ToString(); } } - } -- cgit v1.2.3 From 8d3b5c851ded74b9f34eb2cd963187761a3f6f61 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 9 Jun 2019 22:08:01 +0200 Subject: Improvements to UserManager --- Emby.Notifications/NotificationManager.cs | 2 +- Emby.Server.Implementations/ApplicationHost.cs | 3 +- .../Data/SqliteUserDataRepository.cs | 4 +- .../EntryPoints/RefreshUsersMetadata.cs | 4 +- .../Library/DefaultPasswordResetProvider.cs | 44 +-- Emby.Server.Implementations/Library/UserManager.cs | 358 ++++++++++----------- .../Session/SessionManager.cs | 8 +- MediaBrowser.Api/UserService.cs | 11 +- .../Authentication/AuthenticationException.cs | 1 + MediaBrowser.Controller/Entities/User.cs | 66 +--- MediaBrowser.Controller/Library/IUserManager.cs | 10 +- 11 files changed, 237 insertions(+), 274 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Notifications/NotificationManager.cs b/Emby.Notifications/NotificationManager.cs index a767e541e..eecbbea07 100644 --- a/Emby.Notifications/NotificationManager.cs +++ b/Emby.Notifications/NotificationManager.cs @@ -89,7 +89,7 @@ namespace Emby.Notifications return _userManager.Users.Where(i => i.Policy.IsAdministrator) .Select(i => i.Id); case SendToUserType.All: - return _userManager.Users.Select(i => i.Id); + return _userManager.UsersIds; case SendToUserType.Custom: return request.UserIds; default: diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 0b3b81f94..c390ba635 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -770,7 +770,8 @@ namespace Emby.Server.Implementations _userRepository = GetUserRepository(); - UserManager = new UserManager(LoggerFactory, ServerConfigurationManager, _userRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager); + UserManager = new UserManager(LoggerFactory.CreateLogger(), _userRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager); + serviceCollection.AddSingleton(UserManager); LibraryManager = new LibraryManager(this, LoggerFactory, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager); diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 4035bb99d..9d4855bcf 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Data var userDatasTableExists = TableExists(connection, "UserDatas"); var userDataTableExists = TableExists(connection, "userdata"); - var users = userDatasTableExists ? null : userManager.Users.ToArray(); + var users = userDatasTableExists ? null : userManager.Users; connection.RunInTransaction(db => { @@ -84,7 +84,7 @@ namespace Emby.Server.Implementations.Data } } - private void ImportUserIds(IDatabaseConnection db, User[] users) + private void ImportUserIds(IDatabaseConnection db, IEnumerable users) { var userIdsWithUserData = GetAllUserIdsWithUserData(db); diff --git a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs index b7565adec..b2328121e 100644 --- a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs +++ b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs @@ -50,9 +50,7 @@ namespace Emby.Server.Implementations.EntryPoints public async Task Execute(CancellationToken cancellationToken, IProgress progress) { - var users = _userManager.Users.ToList(); - - foreach (var user in users) + foreach (var user in _userManager.Users) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs index c7044820c..fa6bbcf91 100644 --- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs @@ -1,15 +1,12 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; -using System.Linq; -using System.Text; +using System.Security.Cryptography; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; @@ -17,32 +14,37 @@ namespace Emby.Server.Implementations.Library { public class DefaultPasswordResetProvider : IPasswordResetProvider { - public string Name => "Default Password Reset Provider"; + private const string BaseResetFileName = "passwordreset"; - public bool IsEnabled => true; + private readonly IJsonSerializer _jsonSerializer; + private readonly IUserManager _userManager; private readonly string _passwordResetFileBase; private readonly string _passwordResetFileBaseDir; - private readonly string _passwordResetFileBaseName = "passwordreset"; - private readonly IJsonSerializer _jsonSerializer; - private readonly IUserManager _userManager; - private readonly ICryptoProvider _crypto; - - public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager, ICryptoProvider cryptoProvider) + public DefaultPasswordResetProvider( + IServerConfigurationManager configurationManager, + IJsonSerializer jsonSerializer, + IUserManager userManager) { _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; - _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName); + _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName); _jsonSerializer = jsonSerializer; _userManager = userManager; - _crypto = cryptoProvider; } + /// + public string Name => "Default Password Reset Provider"; + + /// + public bool IsEnabled => true; + + /// public async Task RedeemPasswordResetPin(string pin) { SerializablePasswordReset spr; - HashSet usersreset = new HashSet(); - foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*")) + List usersreset = new List(); + foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*")) { using (var str = File.OpenRead(resetfile)) { @@ -53,12 +55,15 @@ namespace Emby.Server.Implementations.Library { File.Delete(resetfile); } - else if (spr.Pin.Replace("-", "").Equals(pin.Replace("-", ""), StringComparison.InvariantCultureIgnoreCase)) + else if (string.Equals( + spr.Pin.Replace("-", string.Empty), + pin.Replace("-", string.Empty), + StringComparison.InvariantCultureIgnoreCase)) { var resetUser = _userManager.GetUserByName(spr.UserName); if (resetUser == null) { - throw new Exception($"User with a username of {spr.UserName} not found"); + throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found"); } await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); @@ -81,10 +86,11 @@ namespace Emby.Server.Implementations.Library } } + /// public async Task StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork) { string pin = string.Empty; - using (var cryptoRandom = System.Security.Cryptography.RandomNumberGenerator.Create()) + using (var cryptoRandom = RandomNumberGenerator.Create()) { byte[] bytes = new byte[4]; cryptoRandom.GetBytes(bytes); diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index c8c8a108d..086527883 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -11,13 +12,11 @@ using MediaBrowser.Common.Events; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; @@ -40,35 +39,20 @@ namespace Emby.Server.Implementations.Library /// public class UserManager : IUserManager { - /// - /// Gets the users. - /// - /// The users. - public IEnumerable Users => _users; - - private User[] _users; - /// /// The _logger /// private readonly ILogger _logger; - /// - /// Gets or sets the configuration manager. - /// - /// The configuration manager. - private IServerConfigurationManager ConfigurationManager { get; set; } + private readonly object _policySyncLock = new object(); /// /// Gets the active user repository /// /// The user repository. - private IUserRepository UserRepository { get; set; } - public event EventHandler> UserPasswordChanged; - + private readonly IUserRepository _userRepository; private readonly IXmlSerializer _xmlSerializer; private readonly IJsonSerializer _jsonSerializer; - private readonly INetworkManager _networkManager; private readonly Func _imageProcessorFactory; @@ -76,6 +60,8 @@ namespace Emby.Server.Implementations.Library private readonly IServerApplicationHost _appHost; private readonly IFileSystem _fileSystem; + private ConcurrentDictionary _users; + private IAuthenticationProvider[] _authenticationProviders; private DefaultAuthenticationProvider _defaultAuthenticationProvider; @@ -85,8 +71,7 @@ namespace Emby.Server.Implementations.Library private DefaultPasswordResetProvider _defaultPasswordResetProvider; public UserManager( - ILoggerFactory loggerFactory, - IServerConfigurationManager configurationManager, + ILogger logger, IUserRepository userRepository, IXmlSerializer xmlSerializer, INetworkManager networkManager, @@ -96,8 +81,8 @@ namespace Emby.Server.Implementations.Library IJsonSerializer jsonSerializer, IFileSystem fileSystem) { - _logger = loggerFactory.CreateLogger(nameof(UserManager)); - UserRepository = userRepository; + _logger = logger; + _userRepository = userRepository; _xmlSerializer = xmlSerializer; _networkManager = networkManager; _imageProcessorFactory = imageProcessorFactory; @@ -105,8 +90,51 @@ namespace Emby.Server.Implementations.Library _appHost = appHost; _jsonSerializer = jsonSerializer; _fileSystem = fileSystem; - ConfigurationManager = configurationManager; - _users = Array.Empty(); + _users = null; + } + + public event EventHandler> UserPasswordChanged; + + /// + /// Occurs when [user updated]. + /// + public event EventHandler> UserUpdated; + + public event EventHandler> UserPolicyUpdated; + + public event EventHandler> UserConfigurationUpdated; + + public event EventHandler> UserLockedOut; + + public event EventHandler> UserCreated; + + /// + /// Occurs when [user deleted]. + /// + public event EventHandler> UserDeleted; + + /// + public IEnumerable Users => _users.Values; + + /// + public IEnumerable UsersIds => _users.Keys; + + /// + /// Called when [user updated]. + /// + /// The user. + private void OnUserUpdated(User user) + { + UserUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); + } + + /// + /// Called when [user deleted]. + /// + /// The user. + private void OnUserDeleted(User user) + { + UserDeleted?.Invoke(this, new GenericEventArgs { Argument = user }); } public NameIdPair[] GetAuthenticationProviders() @@ -137,7 +165,7 @@ namespace Emby.Server.Implementations.Library .ToArray(); } - public void AddParts(IEnumerable authenticationProviders,IEnumerable passwordResetProviders) + public void AddParts(IEnumerable authenticationProviders, IEnumerable passwordResetProviders) { _authenticationProviders = authenticationProviders.ToArray(); @@ -150,54 +178,21 @@ namespace Emby.Server.Implementations.Library _defaultPasswordResetProvider = passwordResetProviders.OfType().First(); } - #region UserUpdated Event /// - /// Occurs when [user updated]. - /// - public event EventHandler> UserUpdated; - public event EventHandler> UserPolicyUpdated; - public event EventHandler> UserConfigurationUpdated; - public event EventHandler> UserLockedOut; - - /// - /// Called when [user updated]. - /// - /// The user. - private void OnUserUpdated(User user) - { - UserUpdated?.Invoke(this, new GenericEventArgs { Argument = user }); - } - #endregion - - #region UserDeleted Event - /// - /// Occurs when [user deleted]. - /// - public event EventHandler> UserDeleted; - /// - /// Called when [user deleted]. - /// - /// The user. - private void OnUserDeleted(User user) - { - UserDeleted?.Invoke(this, new GenericEventArgs { Argument = user }); - } - #endregion - - /// - /// Gets a User by Id + /// Gets a User by Id. /// /// The id. /// User. - /// + /// public User GetUserById(Guid id) { if (id == Guid.Empty) { - throw new ArgumentException(nameof(id), "Guid can't be empty"); + throw new ArgumentException("Guid can't be empty", nameof(id)); } - return Users.FirstOrDefault(u => u.Id == id); + _users.TryGetValue(id, out User user); + return user; } /// @@ -206,15 +201,13 @@ namespace Emby.Server.Implementations.Library /// The identifier. /// User. public User GetUserById(string id) - { - return GetUserById(new Guid(id)); - } + => GetUserById(new Guid(id)); public User GetUserByName(string name) { if (string.IsNullOrWhiteSpace(name)) { - throw new ArgumentNullException(nameof(name)); + throw new ArgumentException("Invalid username", nameof(name)); } return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase)); @@ -222,8 +215,9 @@ namespace Emby.Server.Implementations.Library public void Initialize() { - var users = LoadUsers(); - _users = users.ToArray(); + LoadUsers(); + + var users = Users; // If there are no local users with admin rights, make them all admins if (!users.Any(i => i.Policy.IsAdministrator)) @@ -240,14 +234,12 @@ namespace Emby.Server.Implementations.Library { // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness - // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.) + // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.) return Regex.IsMatch(username, @"^[\w\-'._@]*$"); } private static bool IsValidUsernameCharacter(char i) - { - return IsValidUsername(i.ToString()); - } + => IsValidUsername(i.ToString(CultureInfo.InvariantCulture)); public string MakeValidUsername(string username) { @@ -277,8 +269,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(username)); } - var user = Users - .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); + var user = Users.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); var success = false; string updatedUsername = null; @@ -299,13 +290,12 @@ namespace Emby.Server.Implementations.Library updatedUsername = authResult.username; success = authResult.success; - if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) + if (success + && authenticationProvider != null + && !(authenticationProvider is DefaultAuthenticationProvider)) { // We should trust the user that the authprovider says, not what was typed - if (updatedUsername != username) - { - username = updatedUsername; - } + username = updatedUsername; // Search the database for the user again; the authprovider might have created it user = Users @@ -337,10 +327,11 @@ namespace Emby.Server.Implementations.Library if (user.Policy.IsDisabled) { - throw new AuthenticationException(string.Format( - CultureInfo.InvariantCulture, - "The {0} account is currently disabled. Please consult with your administrator.", - user.Name)); + throw new AuthenticationException( + string.Format( + CultureInfo.InvariantCulture, + "The {0} account is currently disabled. Please consult with your administrator.", + user.Name)); } if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint)) @@ -386,7 +377,7 @@ namespace Emby.Server.Implementations.Library private IAuthenticationProvider GetAuthenticationProvider(User user) { - return GetAuthenticationProviders(user).First(); + return GetAuthenticationProviders(user)[0]; } private IPasswordResetProvider GetPasswordResetProvider(User user) @@ -396,7 +387,7 @@ namespace Emby.Server.Implementations.Library private IAuthenticationProvider[] GetAuthenticationProviders(User user) { - var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId; + var authenticationProviderId = user?.Policy.AuthenticationProviderId; var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray(); @@ -438,16 +429,10 @@ namespace Emby.Server.Implementations.Library { try { - var requiresResolvedUser = provider as IRequiresResolvedUser; - ProviderAuthenticationResult authenticationResult = null; - if (requiresResolvedUser != null) - { - authenticationResult = await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); - } - else - { - authenticationResult = await provider.Authenticate(username, password).ConfigureAwait(false); - } + + var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser + ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false) + : await provider.Authenticate(username, password).ConfigureAwait(false); if (authenticationResult.Username != username) { @@ -467,7 +452,6 @@ namespace Emby.Server.Implementations.Library private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) { - string updatedUsername = null; bool success = false; IAuthenticationProvider authenticationProvider = null; @@ -487,7 +471,7 @@ namespace Emby.Server.Implementations.Library foreach (var provider in GetAuthenticationProviders(user)) { var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); - updatedUsername = providerAuthResult.username; + var updatedUsername = providerAuthResult.username; success = providerAuthResult.success; if (success) @@ -499,25 +483,32 @@ namespace Emby.Server.Implementations.Library } } - if (user != null) + if (user != null + && !success + && _networkManager.IsInLocalNetwork(remoteEndPoint) + && user.Configuration.EnableLocalPassword) { - if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword) + if (password == null) { - if (password == null) - { - // legacy - success = string.Equals(GetAuthenticationProvider(user).GetEasyPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); - } - else - { - success = string.Equals(GetAuthenticationProvider(user).GetEasyPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); - } + // legacy + success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + } + else + { + success = string.Equals(GetLocalPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); } } return (authenticationProvider, username, success); } + private string GetLocalPasswordHash(User user) + { + return string.IsNullOrEmpty(user.EasyPassword) + ? null + : PasswordHash.ConvertToByteString(new PasswordHash(user.EasyPassword).Hash); + } + private void UpdateInvalidLoginAttemptCount(User user, int newValue) { if (user.Policy.InvalidLoginAttemptCount == newValue || newValue <= 0) @@ -556,17 +547,17 @@ namespace Emby.Server.Implementations.Library } /// - /// Loads the users from the repository + /// Loads the users from the repository. /// - /// IEnumerable{User}. - private List LoadUsers() + private void LoadUsers() { - var users = UserRepository.RetrieveAllUsers(); + var users = _userRepository.RetrieveAllUsers(); // There always has to be at least one user. if (users.Count != 0) { - return users; + _users = new ConcurrentDictionary( + users.Select(x => new KeyValuePair(x.Id, x))); } var defaultName = Environment.UserName; @@ -581,14 +572,15 @@ namespace Emby.Server.Implementations.Library user.DateLastSaved = DateTime.UtcNow; - UserRepository.CreateUser(user); + _userRepository.CreateUser(user); user.Policy.IsAdministrator = true; user.Policy.EnableContentDeletion = true; user.Policy.EnableRemoteControlOfOtherUsers = true; UpdateUserPolicy(user, user.Policy, false); - return new List { user }; + _users = new ConcurrentDictionary(); + _users[user.Id] = user; } public UserDto GetUserDto(User user, string remoteEndPoint = null) @@ -619,7 +611,7 @@ namespace Emby.Server.Implementations.Library Policy = user.Policy }; - if (!hasPassword && Users.Count() == 1) + if (!hasPassword && _users.Count == 1) { dto.EnableAutoLogin = true; } @@ -694,22 +686,26 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(user)); } - if (string.IsNullOrEmpty(newName)) + if (string.IsNullOrWhiteSpace(newName)) { - throw new ArgumentNullException(nameof(newName)); + throw new ArgumentException("Invalid username", nameof(newName)); } - if (Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) + if (user.Name.Equals(newName, StringComparison.OrdinalIgnoreCase)) { - throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName)); + throw new ArgumentException("The new and old names must be different."); } - if (user.Name.Equals(newName, StringComparison.Ordinal)) + if (Users.Any( + u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) { - throw new ArgumentException("The new and old names must be different."); + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "A user with the name '{0}' already exists.", + newName)); } - await user.Rename(newName); + await user.Rename(newName).ConfigureAwait(false); OnUserUpdated(user); } @@ -727,23 +723,30 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(user)); } - if (user.Id.Equals(Guid.Empty) || !Users.Any(u => u.Id.Equals(user.Id))) + if (user.Id == Guid.Empty) { - throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id)); + throw new ArgumentException("Id can't be empty.", nameof(user)); + } + + if (!_users.ContainsKey(user.Id)) + { + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "A user '{0}' with Id {1} does not exist.", + user.Name, + user.Id), + nameof(user)); } user.DateModified = DateTime.UtcNow; user.DateLastSaved = DateTime.UtcNow; - UserRepository.UpdateUser(user); + _userRepository.UpdateUser(user); OnUserUpdated(user); } - public event EventHandler> UserCreated; - - private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1); - /// /// Creates the user. /// @@ -751,7 +754,7 @@ namespace Emby.Server.Implementations.Library /// User. /// name /// - public async Task CreateUser(string name) + public User CreateUser(string name) { if (string.IsNullOrWhiteSpace(name)) { @@ -768,28 +771,17 @@ namespace Emby.Server.Implementations.Library throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name)); } - await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); - - try - { - var user = InstantiateNewUser(name); + var user = InstantiateNewUser(name); - var list = Users.ToList(); - list.Add(user); - _users = list.ToArray(); + _users[user.Id] = user; - user.DateLastSaved = DateTime.UtcNow; + user.DateLastSaved = DateTime.UtcNow; - UserRepository.CreateUser(user); + _userRepository.CreateUser(user); - EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs { Argument = user }, _logger); + EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs { Argument = user }, _logger); - return user; - } - finally - { - _userListLock.Release(); - } + return user; } /// @@ -799,57 +791,59 @@ namespace Emby.Server.Implementations.Library /// Task. /// user /// - public async Task DeleteUser(User user) + public void DeleteUser(User user) { if (user == null) { throw new ArgumentNullException(nameof(user)); } - var allUsers = Users.ToList(); - - if (allUsers.FirstOrDefault(u => u.Id == user.Id) == null) + if (!_users.ContainsKey(user.Id)) { - throw new ArgumentException(string.Format("The user cannot be deleted because there is no user with the Name {0} and Id {1}.", user.Name, user.Id)); + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "The user cannot be deleted because there is no user with the Name {0} and Id {1}.", + user.Name, + user.Id)); } - if (allUsers.Count == 1) + if (_users.Count == 1) { - throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one user in the system.", user.Name)); + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "The user '{0}' cannot be deleted because there must be at least one user in the system.", + user.Name)); } - if (user.Policy.IsAdministrator && allUsers.Count(i => i.Policy.IsAdministrator) == 1) + if (user.Policy.IsAdministrator + && Users.Count(i => i.Policy.IsAdministrator) == 1) { - throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one admin user in the system.", user.Name)); + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "The user '{0}' cannot be deleted because there must be at least one admin user in the system.", + user.Name), + nameof(user)); } - await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); + var configPath = GetConfigurationFilePath(user); + + _userRepository.DeleteUser(user); try { - var configPath = GetConfigurationFilePath(user); - - UserRepository.DeleteUser(user); - - try - { - _fileSystem.DeleteFile(configPath); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting file {path}", configPath); - } - - DeleteUserPolicy(user); - - _users = allUsers.Where(i => i.Id != user.Id).ToArray(); - - OnUserDeleted(user); + _fileSystem.DeleteFile(configPath); } - finally + catch (IOException ex) { - _userListLock.Release(); + _logger.LogError(ex, "Error deleting file {path}", configPath); } + + DeleteUserPolicy(user); + + _users.TryRemove(user.Id, out _); + + OnUserDeleted(user); } /// @@ -906,8 +900,7 @@ namespace Emby.Server.Implementations.Library Name = name, Id = Guid.NewGuid(), DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, - UsesIdForConfigurationPath = true + DateModified = DateTime.UtcNow }; } @@ -989,7 +982,6 @@ namespace Emby.Server.Implementations.Library }; } - private readonly object _policySyncLock = new object(); public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy) { var user = GetUserById(userId); diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 0347100a4..61329160a 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1375,16 +1375,14 @@ namespace Emby.Server.Implementations.Session CheckDisposed(); User user = null; - if (!request.UserId.Equals(Guid.Empty)) + if (request.UserId != Guid.Empty) { - user = _userManager.Users - .FirstOrDefault(i => i.Id == request.UserId); + user = _userManager.GetUserById(request.UserId); } if (user == null) { - user = _userManager.Users - .FirstOrDefault(i => string.Equals(request.Username, i.Name, StringComparison.OrdinalIgnoreCase)); + user = _userManager.GetUserByName(request.Username); } if (user != null) diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs index fa70a52aa..21a94a4e0 100644 --- a/MediaBrowser.Api/UserService.cs +++ b/MediaBrowser.Api/UserService.cs @@ -365,8 +365,8 @@ namespace MediaBrowser.Api } _sessionMananger.RevokeUserTokens(user.Id, null); - - return _userManager.DeleteUser(user); + _userManager.DeleteUser(user); + return Task.CompletedTask; } /// @@ -503,9 +503,14 @@ namespace MediaBrowser.Api } } + /// + /// Posts the specified request. + /// + /// The request. + /// System.Object. public async Task Post(CreateUserByName request) { - var newUser = await _userManager.CreateUser(request.Name).ConfigureAwait(false); + var newUser = _userManager.CreateUser(request.Name); // no need to authenticate password for new user if (request.Password != null) diff --git a/MediaBrowser.Controller/Authentication/AuthenticationException.cs b/MediaBrowser.Controller/Authentication/AuthenticationException.cs index 045cbcdae..62eca3ea9 100644 --- a/MediaBrowser.Controller/Authentication/AuthenticationException.cs +++ b/MediaBrowser.Controller/Authentication/AuthenticationException.cs @@ -1,4 +1,5 @@ using System; + namespace MediaBrowser.Controller.Authentication { /// diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs index 968d72579..7d245d4aa 100644 --- a/MediaBrowser.Controller/Entities/User.cs +++ b/MediaBrowser.Controller/Entities/User.cs @@ -17,13 +17,6 @@ namespace MediaBrowser.Controller.Entities public class User : BaseItem { public static IUserManager UserManager { get; set; } - public static IXmlSerializer XmlSerializer { get; set; } - - /// - /// From now on all user paths will be Id-based. - /// This is for backwards compatibility. - /// - public bool UsesIdForConfigurationPath { get; set; } /// /// Gets or sets the password. @@ -31,7 +24,6 @@ namespace MediaBrowser.Controller.Entities /// The password. public string Password { get; set; } public string EasyPassword { get; set; } - public string Salt { get; set; } // Strictly to remove IgnoreDataMember public override ItemImageInfo[] ImageInfos @@ -148,46 +140,23 @@ namespace MediaBrowser.Controller.Entities /// public Task Rename(string newName) { - if (string.IsNullOrEmpty(newName)) - { - throw new ArgumentNullException(nameof(newName)); - } - - // If only the casing is changing, leave the file system alone - if (!UsesIdForConfigurationPath && !string.Equals(newName, Name, StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrWhiteSpace(newName)) { - UsesIdForConfigurationPath = true; - - // Move configuration - var newConfigDirectory = GetConfigurationDirectoryPath(newName); - var oldConfigurationDirectory = ConfigurationDirectoryPath; - - // Exceptions will be thrown if these paths already exist - if (Directory.Exists(newConfigDirectory)) - { - Directory.Delete(newConfigDirectory, true); - } - - if (Directory.Exists(oldConfigurationDirectory)) - { - Directory.Move(oldConfigurationDirectory, newConfigDirectory); - } - else - { - Directory.CreateDirectory(newConfigDirectory); - } + throw new ArgumentException("Username can't be empty", nameof(newName)); } Name = newName; - return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)) - { - ReplaceAllMetadata = true, - ImageRefreshMode = MetadataRefreshMode.FullRefresh, - MetadataRefreshMode = MetadataRefreshMode.FullRefresh, - ForceSave = true + return RefreshMetadata( + new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)) + { + ReplaceAllMetadata = true, + ImageRefreshMode = MetadataRefreshMode.FullRefresh, + MetadataRefreshMode = MetadataRefreshMode.FullRefresh, + ForceSave = true - }, CancellationToken.None); + }, + CancellationToken.None); } public override void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) @@ -216,19 +185,6 @@ namespace MediaBrowser.Controller.Entities { var parentPath = ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath; - // Legacy - if (!UsesIdForConfigurationPath) - { - if (string.IsNullOrEmpty(username)) - { - throw new ArgumentNullException(nameof(username)); - } - - var safeFolderName = FileSystem.GetValidFilename(username); - - return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, safeFolderName); - } - // TODO: Remove idPath and just use usernamePath for future releases var usernamePath = System.IO.Path.Combine(parentPath, username); var idPath = System.IO.Path.Combine(parentPath, Id.ToString("N", CultureInfo.InvariantCulture)); diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index 7f7370893..bbedc0442 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -22,6 +22,12 @@ namespace MediaBrowser.Controller.Library /// The users. IEnumerable Users { get; } + /// + /// Gets the user ids. + /// + /// The users ids. + IEnumerable UsersIds { get; } + /// /// Occurs when [user updated]. /// @@ -92,7 +98,7 @@ namespace MediaBrowser.Controller.Library /// User. /// name /// - Task CreateUser(string name); + User CreateUser(string name); /// /// Deletes the user. @@ -101,7 +107,7 @@ namespace MediaBrowser.Controller.Library /// Task. /// user /// - Task DeleteUser(User user); + void DeleteUser(User user); /// /// Resets the password. -- cgit v1.2.3 From 24fac4b19172eae6a46208d712de09ac97e59d07 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 18 Aug 2019 20:12:25 +0200 Subject: Fix UserNotFoundError --- Emby.Server.Implementations/Library/UserManager.cs | 1 + 1 file changed, 1 insertion(+) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 086527883..a7ea13ca6 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -558,6 +558,7 @@ namespace Emby.Server.Implementations.Library { _users = new ConcurrentDictionary( users.Select(x => new KeyValuePair(x.Id, x))); + return; } var defaultName = Environment.UserName; -- cgit v1.2.3 From 221b831bb23f73437594f2f760bb1e0700e77882 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Fri, 13 Sep 2019 17:16:33 +0200 Subject: Reset invalid login counter on successfull login --- Emby.Server.Implementations/Library/UserManager.cs | 47 ++++++++-------------- MediaBrowser.Model/Users/UserPolicy.cs | 2 +- 2 files changed, 18 insertions(+), 31 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index a7ea13ca6..afa53ff37 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -353,11 +353,11 @@ namespace Emby.Server.Implementations.Library UpdateUser(user); } - UpdateInvalidLoginAttemptCount(user, 0); + ResetInvalidLoginAttemptCount(user); } else { - UpdateInvalidLoginAttemptCount(user, user.Policy.InvalidLoginAttemptCount + 1); + IncrementInvalidLoginAttemptCount(user); } _logger.LogInformation("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied"); @@ -509,41 +509,28 @@ namespace Emby.Server.Implementations.Library : PasswordHash.ConvertToByteString(new PasswordHash(user.EasyPassword).Hash); } - private void UpdateInvalidLoginAttemptCount(User user, int newValue) + private void ResetInvalidLoginAttemptCount(User user) { - if (user.Policy.InvalidLoginAttemptCount == newValue || newValue <= 0) - { - return; - } - - user.Policy.InvalidLoginAttemptCount = newValue; - - // Check for users without a value here and then fill in the default value - // also protect from an always lockout if misconfigured - if (user.Policy.LoginAttemptsBeforeLockout == null || user.Policy.LoginAttemptsBeforeLockout == 0) - { - user.Policy.LoginAttemptsBeforeLockout = user.Policy.IsAdministrator ? 5 : 3; - } - - var maxCount = user.Policy.LoginAttemptsBeforeLockout; - - var fireLockout = false; + user.Policy.InvalidLoginAttemptCount = 0; + UpdateUserPolicy(user, user.Policy, false); + } - // -1 can be used to specify no lockout value - if (maxCount != -1 && newValue >= maxCount) + private void IncrementInvalidLoginAttemptCount(User user) + { + int invalidLogins = ++user.Policy.InvalidLoginAttemptCount; + int maxInvalidLogins = user.Policy.LoginAttemptsBeforeLockout; + if (maxInvalidLogins > 0 + && invalidLogins >= maxInvalidLogins) { - _logger.LogDebug("Disabling user {0} due to {1} unsuccessful login attempts.", user.Name, newValue); user.Policy.IsDisabled = true; - - fireLockout = true; + UserLockedOut?.Invoke(this, new GenericEventArgs(user)); + _logger.LogWarning( + "Disabling user {UserName} due to {Attempts} unsuccessful login attempts.", + user.Name, + invalidLogins); } UpdateUserPolicy(user, user.Policy, false); - - if (fireLockout) - { - UserLockedOut?.Invoke(this, new GenericEventArgs(user)); - } } /// diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index f63ab2bb4..9336c720f 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -66,7 +66,7 @@ namespace MediaBrowser.Model.Users public bool EnableAllFolders { get; set; } public int InvalidLoginAttemptCount { get; set; } - public int? LoginAttemptsBeforeLockout { get; set; } + public int LoginAttemptsBeforeLockout { get; set; } public bool EnablePublicSharing { get; set; } -- cgit v1.2.3 From 6f17a0b7af5775386e554f2e2e2a4a6829d2895d Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 17 Sep 2019 18:07:15 +0200 Subject: Remove legacy auth code (#1677) * Remove legacy auth code * Adds tests so we don't break PasswordHash (again) * Clean up interfaces * Remove duplicate code * Use auto properties * static using * Don't use 'this' * Fix build --- .../Cryptography/CryptographyProvider.cs | 65 +- .../Library/DefaultAuthenticationProvider.cs | 134 +- Emby.Server.Implementations/Library/UserManager.cs | 58 +- .../Updates/InstallationManager.cs | 4 +- MediaBrowser.Api/StartupWizardService.cs | 3 +- MediaBrowser.Common/Cryptography/Constants.cs | 18 + MediaBrowser.Common/Cryptography/Extensions.cs | 35 + MediaBrowser.Common/Cryptography/PasswordHash.cs | 155 +++ MediaBrowser.Common/Extensions/HexHelper.cs | 22 - MediaBrowser.Common/HexHelper.cs | 22 + MediaBrowser.Common/MediaBrowser.Common.csproj | 6 + MediaBrowser.Model/Cryptography/ICryptoProvider.cs | 19 +- MediaBrowser.Model/Cryptography/PasswordHash.cs | 142 --- .../Resources/SampleTransformed.htm | 1277 -------------------- .../ConsistencyTests/Resources/StringCheck.xslt | 145 --- .../Resources/StringCheckSample.xml | 222 ---- .../ConsistencyTests/StringUsageReporter.cs | 259 ---- .../ConsistencyTests/TextIndexing/IndexBuilder.cs | 52 - .../ConsistencyTests/TextIndexing/WordIndex.cs | 36 - .../TextIndexing/WordOccurrence.cs | 18 - .../TextIndexing/WordOccurrences.cs | 13 - MediaBrowser.Tests/M3uParserTest.cs | 92 -- MediaBrowser.Tests/MediaBrowser.Tests.csproj | 139 --- .../MediaEncoding/Subtitles/AssParserTests.cs | 86 -- .../MediaEncoding/Subtitles/SrtParserTests.cs | 114 -- .../MediaEncoding/Subtitles/TestSubtitles/data.ass | 23 - .../Subtitles/TestSubtitles/data2.ass | 391 ------ .../Subtitles/TestSubtitles/expected.vtt | 32 - .../MediaEncoding/Subtitles/TestSubtitles/unit.srt | 44 - .../MediaEncoding/Subtitles/VttWriterTest.cs | 105 -- MediaBrowser.Tests/Properties/AssemblyInfo.cs | 23 - MediaBrowser.Tests/app.config | 11 - MediaBrowser.sln | 11 + .../Jellyfin.Common.Tests.csproj | 19 + tests/Jellyfin.Common.Tests/PasswordHashTests.cs | 29 + 35 files changed, 374 insertions(+), 3450 deletions(-) create mode 100644 MediaBrowser.Common/Cryptography/Constants.cs create mode 100644 MediaBrowser.Common/Cryptography/Extensions.cs create mode 100644 MediaBrowser.Common/Cryptography/PasswordHash.cs delete mode 100644 MediaBrowser.Common/Extensions/HexHelper.cs create mode 100644 MediaBrowser.Common/HexHelper.cs delete mode 100644 MediaBrowser.Model/Cryptography/PasswordHash.cs delete mode 100644 MediaBrowser.Tests/ConsistencyTests/Resources/SampleTransformed.htm delete mode 100644 MediaBrowser.Tests/ConsistencyTests/Resources/StringCheck.xslt delete mode 100644 MediaBrowser.Tests/ConsistencyTests/Resources/StringCheckSample.xml delete mode 100644 MediaBrowser.Tests/ConsistencyTests/StringUsageReporter.cs delete mode 100644 MediaBrowser.Tests/ConsistencyTests/TextIndexing/IndexBuilder.cs delete mode 100644 MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordIndex.cs delete mode 100644 MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrence.cs delete mode 100644 MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrences.cs delete mode 100644 MediaBrowser.Tests/M3uParserTest.cs delete mode 100644 MediaBrowser.Tests/MediaBrowser.Tests.csproj delete mode 100644 MediaBrowser.Tests/MediaEncoding/Subtitles/AssParserTests.cs delete mode 100644 MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs delete mode 100644 MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data.ass delete mode 100644 MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data2.ass delete mode 100644 MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/expected.vtt delete mode 100644 MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/unit.srt delete mode 100644 MediaBrowser.Tests/MediaEncoding/Subtitles/VttWriterTest.cs delete mode 100644 MediaBrowser.Tests/Properties/AssemblyInfo.cs delete mode 100644 MediaBrowser.Tests/app.config create mode 100644 tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj create mode 100644 tests/Jellyfin.Common.Tests/PasswordHashTests.cs (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index f726dae2e..23b77e268 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; -using System.Globalization; -using System.IO; using System.Security.Cryptography; -using System.Text; using MediaBrowser.Model.Cryptography; +using static MediaBrowser.Common.Cryptography.Constants; namespace Emby.Server.Implementations.Cryptography { @@ -30,8 +28,6 @@ namespace Emby.Server.Implementations.Cryptography private RandomNumberGenerator _randomNumberGenerator; - private const int _defaultIterations = 1000; - private bool _disposed = false; public CryptographyProvider() @@ -45,44 +41,13 @@ namespace Emby.Server.Implementations.Cryptography public string DefaultHashMethod => "PBKDF2"; - [Obsolete("Use System.Security.Cryptography.MD5 directly")] - public Guid GetMD5(string str) - => new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str))); - - [Obsolete("Use System.Security.Cryptography.SHA1 directly")] - public byte[] ComputeSHA1(byte[] bytes) - { - using (var provider = SHA1.Create()) - { - return provider.ComputeHash(bytes); - } - } - - [Obsolete("Use System.Security.Cryptography.MD5 directly")] - public byte[] ComputeMD5(Stream str) - { - using (var provider = MD5.Create()) - { - return provider.ComputeHash(str); - } - } - - [Obsolete("Use System.Security.Cryptography.MD5 directly")] - public byte[] ComputeMD5(byte[] bytes) - { - using (var provider = MD5.Create()) - { - return provider.ComputeHash(bytes); - } - } - public IEnumerable GetSupportedHashMethods() => _supportedHashMethods; private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations) { - //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 + // 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) { using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations)) @@ -104,7 +69,7 @@ namespace Emby.Server.Implementations.Cryptography { if (hashMethod == DefaultHashMethod) { - return PBKDF2(hashMethod, bytes, salt, _defaultIterations); + return PBKDF2(hashMethod, bytes, salt, DefaultIterations); } else if (_supportedHashMethods.Contains(hashMethod)) { @@ -129,26 +94,14 @@ namespace Emby.Server.Implementations.Cryptography } public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt) - => PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations); - - public byte[] ComputeHash(PasswordHash hash) - { - int iterations = _defaultIterations; - if (!hash.Parameters.ContainsKey("iterations")) - { - hash.Parameters.Add("iterations", iterations.ToString(CultureInfo.InvariantCulture)); - } - else if (!int.TryParse(hash.Parameters["iterations"], out iterations)) - { - throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}"); - } - - return PBKDF2(hash.Id, hash.Hash, hash.Salt, iterations); - } + => PBKDF2(DefaultHashMethod, bytes, salt, DefaultIterations); public byte[] GenerateSalt() + => GenerateSalt(DefaultSaltLength); + + public byte[] GenerateSalt(int length) { - byte[] salt = new byte[64]; + byte[] salt = new byte[length]; _randomNumberGenerator.GetBytes(salt); return salt; } diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 2282b8efb..c95b00ede 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -2,24 +2,30 @@ using System; using System.Linq; using System.Text; using System.Threading.Tasks; +using MediaBrowser.Common.Cryptography; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Cryptography; +using static MediaBrowser.Common.HexHelper; namespace Emby.Server.Implementations.Library { public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser { private readonly ICryptoProvider _cryptographyProvider; + public DefaultAuthenticationProvider(ICryptoProvider cryptographyProvider) { _cryptographyProvider = cryptographyProvider; } + /// public string Name => "Default"; + /// public bool IsEnabled => true; + /// // This is dumb and an artifact of the backwards way auth providers were designed. // This version of authenticate was never meant to be called, but needs to be here for interface compat // Only the providers that don't provide local user support use this @@ -28,6 +34,7 @@ namespace Emby.Server.Implementations.Library throw new NotImplementedException(); } + /// // This is the version that we need to use for local users. Because reasons. public Task Authenticate(string username, string password, User resolvedUser) { @@ -46,10 +53,9 @@ namespace Emby.Server.Implementations.Library }); } - ConvertPasswordFormat(resolvedUser); byte[] passwordbytes = Encoding.UTF8.GetBytes(password); - PasswordHash readyHash = new PasswordHash(resolvedUser.Password); + PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password); if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id) || _cryptographyProvider.DefaultHashMethod == readyHash.Id) { @@ -76,72 +82,31 @@ namespace Emby.Server.Implementations.Library }); } - // This allows us to move passwords forward to the newformat without breaking. They are still insecure, unsalted, and dumb before a password change - // but at least they are in the new format. - private void ConvertPasswordFormat(User user) - { - if (string.IsNullOrEmpty(user.Password)) - { - return; - } - - if (user.Password.IndexOf('$') == -1) - { - string hash = user.Password; - user.Password = string.Format("$SHA1${0}", hash); - } - - if (user.EasyPassword != null - && user.EasyPassword.IndexOf('$') == -1) - { - string hash = user.EasyPassword; - user.EasyPassword = string.Format("$SHA1${0}", hash); - } - } - + /// public bool HasPassword(User user) => !string.IsNullOrEmpty(user.Password); + /// public Task ChangePassword(User user, string newPassword) { - ConvertPasswordFormat(user); - - // This is needed to support changing a no password user to a password user - if (string.IsNullOrEmpty(user.Password)) + if (string.IsNullOrEmpty(newPassword)) { - PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider); - newPasswordHash.Salt = _cryptographyProvider.GenerateSalt(); - newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod; - newPasswordHash.Hash = GetHashedChangeAuth(newPassword, newPasswordHash); - user.Password = newPasswordHash.ToString(); + user.Password = null; return Task.CompletedTask; } - PasswordHash passwordHash = new PasswordHash(user.Password); - if (passwordHash.Id == "SHA1" - && passwordHash.Salt.Length == 0) - { - passwordHash.Salt = _cryptographyProvider.GenerateSalt(); - passwordHash.Id = _cryptographyProvider.DefaultHashMethod; - passwordHash.Hash = GetHashedChangeAuth(newPassword, passwordHash); - } - else if (newPassword != null) - { - passwordHash.Hash = GetHashed(user, newPassword); - } - - user.Password = passwordHash.ToString(); + PasswordHash newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword); + user.Password = newPasswordHash.ToString(); return Task.CompletedTask; } + /// public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) { - ConvertPasswordFormat(user); - if (newPassword != null) { - newPasswordHash = string.Format("$SHA1${0}", GetHashedString(user, newPassword)); + newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword).ToString(); } if (string.IsNullOrWhiteSpace(newPasswordHash)) @@ -152,21 +117,12 @@ namespace Emby.Server.Implementations.Library user.EasyPassword = newPasswordHash; } + /// public string GetEasyPasswordHash(User user) { - // This should be removed in the future. This was added to let user login after - // Jellyfin 10.3.3 failed to save a well formatted PIN. - ConvertPasswordFormat(user); - return string.IsNullOrEmpty(user.EasyPassword) ? null - : PasswordHash.ConvertToByteString(new PasswordHash(user.EasyPassword).Hash); - } - - internal byte[] GetHashedChangeAuth(string newPassword, PasswordHash passwordHash) - { - passwordHash.Hash = Encoding.UTF8.GetBytes(newPassword); - return _cryptographyProvider.ComputeHash(passwordHash); + : ToHexString(PasswordHash.Parse(user.EasyPassword).Hash); } /// @@ -174,54 +130,36 @@ namespace Emby.Server.Implementations.Library /// public string GetHashedString(User user, string str) { - PasswordHash passwordHash; if (string.IsNullOrEmpty(user.Password)) { - passwordHash = new PasswordHash(_cryptographyProvider); - } - else - { - ConvertPasswordFormat(user); - passwordHash = new PasswordHash(user.Password); + return _cryptographyProvider.CreatePasswordHash(str).ToString(); } - if (passwordHash.Salt != null) - { - // the password is modern format with PBKDF and we should take advantage of that - passwordHash.Hash = Encoding.UTF8.GetBytes(str); - return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash)); - } - else - { - // the password has no salt and should be called with the older method for safety - return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str))); - } + // TODO: make use of iterations parameter? + PasswordHash passwordHash = PasswordHash.Parse(user.Password); + return new PasswordHash( + passwordHash.Id, + _cryptographyProvider.ComputeHash( + passwordHash.Id, + Encoding.UTF8.GetBytes(str), + passwordHash.Salt), + passwordHash.Salt, + passwordHash.Parameters.ToDictionary(x => x.Key, y => y.Value)).ToString(); } public byte[] GetHashed(User user, string str) { - PasswordHash passwordHash; if (string.IsNullOrEmpty(user.Password)) { - passwordHash = new PasswordHash(_cryptographyProvider); - } - else - { - ConvertPasswordFormat(user); - passwordHash = new PasswordHash(user.Password); + return _cryptographyProvider.CreatePasswordHash(str).Hash; } - if (passwordHash.Salt != null) - { - // the password is modern format with PBKDF and we should take advantage of that - passwordHash.Hash = Encoding.UTF8.GetBytes(str); - return _cryptographyProvider.ComputeHash(passwordHash); - } - else - { - // the password has no salt and should be called with the older method for safety - return _cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str)); - } + // TODO: make use of iterations parameter? + PasswordHash passwordHash = PasswordHash.Parse(user.Password); + return _cryptographyProvider.ComputeHash( + passwordHash.Id, + Encoding.UTF8.GetBytes(str), + passwordHash.Salt); } } } diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index afa53ff37..ac6b4a209 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -8,6 +8,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Cryptography; using MediaBrowser.Common.Events; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -23,7 +24,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Events; @@ -31,6 +31,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; using Microsoft.Extensions.Logging; +using static MediaBrowser.Common.HexHelper; namespace Emby.Server.Implementations.Library { @@ -450,53 +451,38 @@ namespace Emby.Server.Implementations.Library } } - private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) + private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> AuthenticateLocalUser( + string username, + string password, + string hashedPassword, + User user, + string remoteEndPoint) { bool success = false; IAuthenticationProvider authenticationProvider = null; - if (password != null && user != null) + foreach (var provider in GetAuthenticationProviders(user)) { - // Doesn't look like this is even possible to be used, because of password == null checks below - hashedPassword = _defaultAuthenticationProvider.GetHashedString(user, password); - } + var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); + var updatedUsername = providerAuthResult.username; + success = providerAuthResult.success; - if (password == null) - { - // legacy - success = string.Equals(user.Password, hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); - } - else - { - foreach (var provider in GetAuthenticationProviders(user)) + if (success) { - var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); - var updatedUsername = providerAuthResult.username; - success = providerAuthResult.success; - - if (success) - { - authenticationProvider = provider; - username = updatedUsername; - break; - } + authenticationProvider = provider; + username = updatedUsername; + break; } } - if (user != null - && !success + if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword) { - if (password == null) - { - // legacy - success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); - } - else - { - success = string.Equals(GetLocalPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); - } + success = string.Equals( + GetLocalPasswordHash(user), + _defaultAuthenticationProvider.GetHashedString(user, password), + StringComparison.OrdinalIgnoreCase); } return (authenticationProvider, username, success); @@ -506,7 +492,7 @@ namespace Emby.Server.Implementations.Library { return string.IsNullOrEmpty(user.EasyPassword) ? null - : PasswordHash.ConvertToByteString(new PasswordHash(user.EasyPassword).Hash); + : ToHexString(PasswordHash.Parse(user.EasyPassword).Hash); } private void ResetInvalidLoginAttemptCount(User user) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 2f84b91ec..7947edeeb 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -9,7 +9,6 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; @@ -19,6 +18,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Updates; using Microsoft.Extensions.Logging; +using static MediaBrowser.Common.HexHelper; namespace Emby.Server.Implementations.Updates { @@ -454,7 +454,7 @@ namespace Emby.Server.Implementations.Updates { cancellationToken.ThrowIfCancellationRequested(); - var hash = HexHelper.ToHexString(md5.ComputeHash(stream)); + var hash = ToHexString(md5.ComputeHash(stream)); if (!string.Equals(package.checksum, hash, StringComparison.OrdinalIgnoreCase)) { _logger.LogDebug("{0}, {1}", package.checksum, hash); diff --git a/MediaBrowser.Api/StartupWizardService.cs b/MediaBrowser.Api/StartupWizardService.cs index 53ba7eefd..3a9eb7a55 100644 --- a/MediaBrowser.Api/StartupWizardService.cs +++ b/MediaBrowser.Api/StartupWizardService.cs @@ -113,7 +113,8 @@ namespace MediaBrowser.Api _userManager.UpdateUser(user); - if (!string.IsNullOrEmpty(request.Password)) { + if (!string.IsNullOrEmpty(request.Password)) + { await _userManager.ChangePassword(user, request.Password).ConfigureAwait(false); } } diff --git a/MediaBrowser.Common/Cryptography/Constants.cs b/MediaBrowser.Common/Cryptography/Constants.cs new file mode 100644 index 000000000..354114232 --- /dev/null +++ b/MediaBrowser.Common/Cryptography/Constants.cs @@ -0,0 +1,18 @@ +namespace MediaBrowser.Common.Cryptography +{ + /// + /// Class containing global constants for Jellyfin Cryptography. + /// + public static class Constants + { + /// + /// The default length for new salts. + /// + public const int DefaultSaltLength = 64; + + /// + /// The default amount of iterations for hashing passwords. + /// + public const int DefaultIterations = 1000; + } +} diff --git a/MediaBrowser.Common/Cryptography/Extensions.cs b/MediaBrowser.Common/Cryptography/Extensions.cs new file mode 100644 index 000000000..1e32a6d1a --- /dev/null +++ b/MediaBrowser.Common/Cryptography/Extensions.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using MediaBrowser.Model.Cryptography; +using static MediaBrowser.Common.Cryptography.Constants; + +namespace MediaBrowser.Common.Cryptography +{ + /// + /// Class containing extension methods for working with Jellyfin cryptography objects. + /// + public static class Extensions + { + /// + /// Creates a new instance. + /// + /// The instance used. + /// The password that will be hashed. + /// A instance with the hash method, hash, salt and number of iterations. + 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 + { + { "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) } + }); + } + } +} diff --git a/MediaBrowser.Common/Cryptography/PasswordHash.cs b/MediaBrowser.Common/Cryptography/PasswordHash.cs new file mode 100644 index 000000000..5b28d344f --- /dev/null +++ b/MediaBrowser.Common/Cryptography/PasswordHash.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using static MediaBrowser.Common.HexHelper; + +namespace MediaBrowser.Common.Cryptography +{ + // Defined from this hash storage spec + // https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md + // $[$=(,=)*][$[$]] + // with one slight amendment to ease the transition, we're writing out the bytes in hex + // rather than making them a BASE64 string with stripped padding + public class PasswordHash + { + private readonly Dictionary _parameters; + + public PasswordHash(string id, byte[] hash) + : this(id, hash, Array.Empty()) + { + + } + + public PasswordHash(string id, byte[] hash, byte[] salt) + : this(id, hash, salt, new Dictionary()) + { + + } + + public PasswordHash(string id, byte[] hash, byte[] salt, Dictionary parameters) + { + Id = id; + Hash = hash; + Salt = salt; + _parameters = parameters; + } + + /// + /// Gets the symbolic name for the function used. + /// + /// Returns the symbolic name for the function used. + public string Id { get; } + + /// + /// Gets the additional parameters used by the hash function. + /// + /// + public IReadOnlyDictionary Parameters => _parameters; + + /// + /// Gets the salt used for hashing the password. + /// + /// Returns the salt used for hashing the password. + public byte[] Salt { get; } + + /// + /// Gets the hashed password. + /// + /// Return the hashed password. + public byte[] Hash { get; } + + public static PasswordHash Parse(string storageString) + { + string[] splitted = storageString.Split('$'); + // The string should at least contain the hash function and the hash itself + if (splitted.Length < 3) + { + throw new ArgumentException("String doesn't contain enough segments", nameof(storageString)); + } + + // Start at 1, the first index shouldn't contain any data + int index = 1; + + // Name of the hash function + string id = splitted[index++]; + + // Optional parameters + Dictionary parameters = new Dictionary(); + if (splitted[index].IndexOf('=') != -1) + { + foreach (string paramset in splitted[index++].Split(',')) + { + if (string.IsNullOrEmpty(paramset)) + { + continue; + } + + string[] fields = paramset.Split('='); + if (fields.Length != 2) + { + throw new InvalidDataException($"Malformed parameter in password hash string {paramset}"); + } + + parameters.Add(fields[0], fields[1]); + } + } + + byte[] hash; + byte[] salt; + // Check if the string also contains a salt + if (splitted.Length - index == 2) + { + salt = FromHexString(splitted[index++]); + hash = FromHexString(splitted[index++]); + } + else + { + salt = Array.Empty(); + hash = FromHexString(splitted[index++]); + } + + return new PasswordHash(id, hash, salt, parameters); + } + + private void SerializeParameters(StringBuilder stringBuilder) + { + if (_parameters.Count == 0) + { + return; + } + + stringBuilder.Append('$'); + foreach (var pair in _parameters) + { + stringBuilder.Append(pair.Key); + stringBuilder.Append('='); + stringBuilder.Append(pair.Value); + stringBuilder.Append(','); + } + + // Remove last ',' + stringBuilder.Length -= 1; + } + + /// + public override string ToString() + { + var str = new StringBuilder(); + str.Append('$'); + str.Append(Id); + SerializeParameters(str); + + if (Salt.Length != 0) + { + str.Append('$'); + str.Append(ToHexString(Salt)); + } + + str.Append('$'); + str.Append(ToHexString(Hash)); + + return str.ToString(); + } + } +} diff --git a/MediaBrowser.Common/Extensions/HexHelper.cs b/MediaBrowser.Common/Extensions/HexHelper.cs deleted file mode 100644 index 3d80d94ac..000000000 --- a/MediaBrowser.Common/Extensions/HexHelper.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Globalization; - -namespace MediaBrowser.Common.Extensions -{ - public static class HexHelper - { - public static byte[] FromHexString(string str) - { - byte[] bytes = new byte[str.Length / 2]; - for (int i = 0; i < str.Length; i += 2) - { - bytes[i / 2] = byte.Parse(str.Substring(i, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture); - } - - return bytes; - } - - public static string ToHexString(byte[] bytes) - => BitConverter.ToString(bytes).Replace("-", ""); - } -} diff --git a/MediaBrowser.Common/HexHelper.cs b/MediaBrowser.Common/HexHelper.cs new file mode 100644 index 000000000..5587c03fd --- /dev/null +++ b/MediaBrowser.Common/HexHelper.cs @@ -0,0 +1,22 @@ +using System; +using System.Globalization; + +namespace MediaBrowser.Common +{ + public static class HexHelper + { + public static byte[] FromHexString(string str) + { + byte[] bytes = new byte[str.Length / 2]; + for (int i = 0; i < str.Length; i += 2) + { + bytes[i / 2] = byte.Parse(str.Substring(i, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture); + } + + return bytes; + } + + public static string ToHexString(byte[] bytes) + => BitConverter.ToString(bytes).Replace("-", ""); + } +} diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 91ab066f9..1a40f5ea2 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -31,4 +31,10 @@ latest + + + <_Parameter1>Jellyfin.Common.Tests + + + diff --git a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs index 9e85beb43..ce6493232 100644 --- a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs +++ b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using System.Collections.Generic; namespace MediaBrowser.Model.Cryptography @@ -7,20 +5,19 @@ namespace MediaBrowser.Model.Cryptography public interface ICryptoProvider { string DefaultHashMethod { get; } - [Obsolete("Use System.Security.Cryptography.MD5 directly")] - Guid GetMD5(string str); - [Obsolete("Use System.Security.Cryptography.MD5 directly")] - byte[] ComputeMD5(Stream str); - [Obsolete("Use System.Security.Cryptography.MD5 directly")] - byte[] ComputeMD5(byte[] bytes); - [Obsolete("Use System.Security.Cryptography.SHA1 directly")] - byte[] ComputeSHA1(byte[] bytes); + IEnumerable GetSupportedHashMethods(); + byte[] ComputeHash(string HashMethod, byte[] bytes); + byte[] ComputeHashWithDefaultMethod(byte[] bytes); + byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt); + byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt); - byte[] ComputeHash(PasswordHash hash); + byte[] GenerateSalt(); + + byte[] GenerateSalt(int length); } } diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs deleted file mode 100644 index 6e66f2088..000000000 --- a/MediaBrowser.Model/Cryptography/PasswordHash.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace MediaBrowser.Model.Cryptography -{ - public class PasswordHash - { - // Defined from this hash storage spec - // https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md - // $[$=(,=)*][$[$]] - // with one slight amendment to ease the transition, we're writing out the bytes in hex - // rather than making them a BASE64 string with stripped padding - - private string _id; - - private Dictionary _parameters = new Dictionary(); - - private byte[] _salt; - - private byte[] _hash; - - public PasswordHash(string storageString) - { - string[] splitted = storageString.Split('$'); - // The string should at least contain the hash function and the hash itself - if (splitted.Length < 3) - { - throw new ArgumentException("String doesn't contain enough segments", nameof(storageString)); - } - - // Start at 1, the first index shouldn't contain any data - int index = 1; - - // Name of the hash function - _id = splitted[index++]; - - // Optional parameters - if (splitted[index].IndexOf('=') != -1) - { - foreach (string paramset in splitted[index++].Split(',')) - { - if (string.IsNullOrEmpty(paramset)) - { - continue; - } - - string[] fields = paramset.Split('='); - if (fields.Length != 2) - { - throw new InvalidDataException($"Malformed parameter in password hash string {paramset}"); - } - - _parameters.Add(fields[0], fields[1]); - } - } - - // Check if the string also contains a salt - if (splitted.Length - index == 2) - { - _salt = ConvertFromByteString(splitted[index++]); - _hash = ConvertFromByteString(splitted[index++]); - } - else - { - _salt = Array.Empty(); - _hash = ConvertFromByteString(splitted[index++]); - } - } - - public PasswordHash(ICryptoProvider cryptoProvider) - { - _id = cryptoProvider.DefaultHashMethod; - _salt = cryptoProvider.GenerateSalt(); - _hash = Array.Empty(); - } - - public string Id { get => _id; set => _id = value; } - - public Dictionary Parameters { get => _parameters; set => _parameters = value; } - - public byte[] Salt { get => _salt; set => _salt = value; } - - public byte[] Hash { get => _hash; set => _hash = value; } - - // TODO: move this class and use the HexHelper class - public static byte[] ConvertFromByteString(string byteString) - { - byte[] bytes = new byte[byteString.Length / 2]; - for (int i = 0; i < byteString.Length; i += 2) - { - // TODO: NetStandard2.1 switch this to use a span instead of a substring. - bytes[i / 2] = Convert.ToByte(byteString.Substring(i, 2), 16); - } - - return bytes; - } - - public static string ConvertToByteString(byte[] bytes) - => BitConverter.ToString(bytes).Replace("-", string.Empty); - - private void SerializeParameters(StringBuilder stringBuilder) - { - if (_parameters.Count == 0) - { - return; - } - - stringBuilder.Append('$'); - foreach (var pair in _parameters) - { - stringBuilder.Append(pair.Key); - stringBuilder.Append('='); - stringBuilder.Append(pair.Value); - stringBuilder.Append(','); - } - - // Remove last ',' - stringBuilder.Length -= 1; - } - - public override string ToString() - { - var str = new StringBuilder(); - str.Append('$'); - str.Append(_id); - SerializeParameters(str); - - if (_salt.Length != 0) - { - str.Append('$'); - str.Append(ConvertToByteString(_salt)); - } - - str.Append('$'); - str.Append(ConvertToByteString(_hash)); - - return str.ToString(); - } - } -} diff --git a/MediaBrowser.Tests/ConsistencyTests/Resources/SampleTransformed.htm b/MediaBrowser.Tests/ConsistencyTests/Resources/SampleTransformed.htm deleted file mode 100644 index f36652468..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/Resources/SampleTransformed.htm +++ /dev/null @@ -1,1277 +0,0 @@ - - - - - String Usage Report - - - -

String Usage Report

-
-

Strings

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- LabelExit: "
-
Exit"
-
-
-
-
- LabelVisitCommunity: "
-
Visit Community"
-
-
-
-
- LabelGithub: "
-
Github"
-
-
-
-
- LabelSwagger: "
-
Swagger"
-
-
-
-
- LabelStandard: "
-
Standard"
-
-
-
-
- LabelApiDocumentation: "
-
Api Documentation"
-
-
-
-
- LabelDeveloperResources: "
-
Developer Resources"
-
-
-
-
- LabelBrowseLibrary: "
-
Browse Library"
-
-
-
-
- LabelConfigureServer: "
-
Configure Emby"
-
-
-
-
- LabelOpenLibraryViewer: "
-
Open Library Viewer"
-
-
-
-
- LabelRestartServer: "
-
Restart Server"
-
-
-
-
- LabelShowLogWindow: "
-
Show Log Window"
-
-
-
-
- LabelPrevious: "
-
Previous"
-
-
- \wizardcomponents.html:54 -
- \wizardfinish.html:40 -
- \wizardlibrary.html:19 -
- \wizardlivetvguide.html:30 -
- \wizardlivetvtuner.html:31 -
- \wizardservice.html:17 -
- \wizardsettings.html:32 -
- \wizarduser.html:27 -
-
-
- LabelFinish: "
-
Finish"
-
-
- \wizardfinish.html:41 -
-
-
- LabelNext: "
-
Next"
-
-
- \wizardcomponents.html:55 -
- \wizardlibrary.html:20 -
- \wizardlivetvguide.html:31 -
- \wizardlivetvtuner.html:32 -
- \wizardservice.html:18 -
- \wizardsettings.html:33 -
- \wizardstart.html:25 -
- \wizarduser.html:28 -
-
-
- LabelYoureDone: "
-
You're Done!"
-
-
- \wizardfinish.html:7 -
-
-
- WelcomeToProject: "
-
Welcome to Emby!"
-
-
- \wizardstart.html:10 -
-
-
- ThisWizardWillGuideYou: "
-
This wizard will help guide you through the setup process. To begin, please select your preferred language."
-
-
- \wizardstart.html:16 -
-
-
- TellUsAboutYourself: "
-
Tell us about yourself"
-
-
- \wizarduser.html:8 -
-
-
- ButtonQuickStartGuide: "
-
Quick start guide"
-
-
- \wizardstart.html:12 -
-
-
- LabelYourFirstName: "
-
Your first name:"
-
-
- \wizarduser.html:14 -
-
-
- MoreUsersCanBeAddedLater: "
-
More users can be added later within the Dashboard."
-
-
- \wizarduser.html:15 -
-
-
- UserProfilesIntro: "
-
Emby includes built-in support for user profiles, enabling each user to have their own display settings, playstate and parental controls."
-
-
- \wizarduser.html:11 -
-
-
- LabelWindowsService: "
-
Windows Service"
-
-
- \wizardservice.html:7 -
-
-
- AWindowsServiceHasBeenInstalled: "
-
A Windows Service has been installed."
-
-
- \wizardservice.html:10 -
-
-
- WindowsServiceIntro1: "
-
Emby Server normally runs as a desktop application with a tray icon, but if you prefer to run it as a background service, it can be started from the windows services control panel instead."
-
-
- \wizardservice.html:12 -
-
-
- WindowsServiceIntro2: "
-
If using the windows service, please note that it cannot be run at the same time as the tray icon, so you'll need to exit the tray in order to run the service. The service will also need to be configured with administrative privileges via the control panel. When running as a service, you will need to ensure that the service account has access to your media folders."
-
-
- \wizardservice.html:14 -
-
-
- WizardCompleted: "
-
That's all we need for now. Emby has begun collecting information about your media library. Check out some of our apps, and then click <b>Finish</b> to view the <b>Server Dashboard</b>."
-
-
- \wizardfinish.html:10 -
-
-
- LabelConfigureSettings: "
-
Configure settings"
-
-
- \wizardsettings.html:8 -
-
-
- LabelEnableVideoImageExtraction: "
-
Enable video image extraction"
-
-
-
-
- VideoImageExtractionHelp: "
-
For videos that don't already have images, and that we're unable to find internet images for. This will add some additional time to the initial library scan but will result in a more pleasing presentation."
-
-
-
-
- LabelEnableChapterImageExtractionForMovies: "
-
Extract chapter image extraction for Movies"
-
-
-
-
- LabelChapterImageExtractionForMoviesHelp: "
-
Extracting chapter images will allow clients to display graphical scene selection menus. The process can be slow, cpu-intensive and may require several gigabytes of space. It runs as a nightly scheduled task, although this is configurable in the scheduled tasks area. It is not recommended to run this task during peak usage hours."
-
-
-
-
- LabelEnableAutomaticPortMapping: "
-
Enable automatic port mapping"
-
-
-
-
- LabelEnableAutomaticPortMappingHelp: "
-
UPnP allows automated router configuration for easy remote access. This may not work with some router models."
-
-
-
-
- HeaderTermsOfService: "
-
Emby Terms of Service"
-
-
-
-
- HeaderDeveloperOptions: "
-
Developer Options"
-
-
- \dashboardgeneral.html:108 -
-
-
- OptionEnableWebClientResponseCache: "
-
Enable web response caching"
-
-
- \dashboardgeneral.html:112 -
-
-
- OptionDisableForDevelopmentHelp: "
-
Configure these as needed for web development purposes."
-
-
- \dashboardgeneral.html:119 -
-
-
- OptionEnableWebClientResourceMinification: "
-
Enable web resource minification"
-
-
- \dashboardgeneral.html:116 -
-
-
- LabelDashboardSourcePath: "
-
Web client source path:"
-
-
- \dashboardgeneral.html:124 -
-
-
- LabelDashboardSourcePathHelp: "
-
If running the server from source, specify the path to the dashboard-ui folder. All web client files will be served from this location."
-
-
- \dashboardgeneral.html:126 -
-
-
- ButtonConvertMedia: "
-
Convert media"
-
-
- \syncactivity.html:22 -
-
-
- ButtonOrganize: "
-
Organize"
-
-
- \autoorganizelog.html:8 -
- \scripts\autoorganizelog.js:293 -
- \scripts\autoorganizelog.js:294 -
- \scripts\autoorganizelog.js:296 -
-
-
- LinkedToEmbyConnect: "
-
Linked to Emby Connect"
-
-
-
-
- HeaderSupporterBenefits: "
-
Emby Premiere Benefits"
-
-
-
-
- HeaderAddUser: "
-
Add User"
-
-
-
-
- LabelAddConnectSupporterHelp: "
-
To add a user who isn't listed, you'll need to first link their account to Emby Connect from their user profile page."
-
-
-
-
- LabelPinCode: "
-
Pin code:"
-
-
-
-
- OptionHideWatchedContentFromLatestMedia: "
-
Hide watched content from latest media"
-
-
- \mypreferenceshome.html:114 -
-
-
- HeaderSync: "
-
Sync"
-
-
- \mysyncsettings.html:7 -
- \scripts\registrationservices.js:175 -
- \useredit.html:82 -
-
-
- ButtonOk: "
-
Ok"
-
-
- \components\directorybrowser\directorybrowser.js:147 -
- \components\fileorganizer\fileorganizer.template.html:45 -
- \components\medialibrarycreator\medialibrarycreator.template.html:30 -
- \components\metadataeditor\personeditor.template.html:33 -
- \dlnaprofile.html:372 -
- \dlnaprofile.html:453 -
- \dlnaprofile.html:504 -
- \dlnaprofile.html:542 -
- \dlnaprofile.html:590 -
- \dlnaprofile.html:630 -
- \dlnaprofile.html:661 -
- \dlnaprofile.html:706 -
- \nowplaying.html:113 -
- \scripts\ratingdialog.js:42 -
-
-
- ButtonCancel: "
-
Cancel"
-
-
- \components\tvproviders\schedulesdirect.template.html:68 -
- \components\tvproviders\xmltv.template.html:48 -
- \connectlogin.html:74 -
- \connectlogin.html:108 -
- \dlnaprofile.html:325 -
- \dlnaprofile.html:375 -
- \dlnaprofile.html:456 -
- \dlnaprofile.html:507 -
- \dlnaprofile.html:545 -
- \dlnaprofile.html:593 -
- \dlnaprofile.html:633 -
- \dlnaprofile.html:664 -
- \dlnaprofile.html:709 -
- \forgotpassword.html:23 -
- \forgotpasswordpin.html:22 -
- \livetvseriestimer.html:62 -
- \livetvtunerprovider-hdhomerun.html:35 -
- \livetvtunerprovider-m3u.html:19 -
- \livetvtunerprovider-satip.html:65 -
- \login.html:27 -
- \notificationsetting.html:64 -
- \scheduledtask.html:85 -
- \scripts\librarylist.js:349 -
- \scripts\mediacontroller.js:167 -
- \scripts\mediacontroller.js:436 -
- \scripts\ratingdialog.js:43 -
- \scripts\site.js:1025 -
- \scripts\userprofilespage.js:198 -
- \syncsettings.html:43 -
- \useredit.html:111 -
- \userlibraryaccess.html:57 -
- \usernew.html:45 -
- \userparentalcontrol.html:101 -
-
-
- ButtonExit: "
-
Exit"
-
-
-
-
- ButtonNew: "
-
New"
-
-
- \components\fileorganizer\fileorganizer.template.html:18 -
- \dlnaprofile.html:107 -
- \dlnaprofile.html:278 -
- \dlnaprofile.html:290 -
- \dlnaprofile.html:296 -
- \dlnaprofile.html:302 -
- \dlnaprofile.html:308 -
- \dlnaprofile.html:314 -
- \dlnaprofiles.html:14 -
- \serversecurity.html:8 -
-
-
- HeaderTaskTriggers: "
-
Task Triggers"
-
-
- \scheduledtask.html:11 -
-
-
- HeaderTV: "
-
TV"
-
-
- \librarysettings.html:113 -
-
-
- HeaderAudio: "
-
Audio"
-
-
- \librarysettings.html:39 -
-
-
- HeaderVideo: "
-
Video"
-
-
- \librarysettings.html:50 -
-
-
- HeaderPaths: "
-
Paths"
-
-
- \dashboard.html:92 -
-
-
- CategorySync: "
-
Sync"
-
-
-
-
- TabPlaylist: "
-
Playlist"
-
-
- \nowplaying.html:20 -
-
-
- HeaderEasyPinCode: "
-
Easy Pin Code"
-
-
- \myprofile.html:69 -
- \userpassword.html:42 -
-
-
- HeaderGrownupsOnly: "
-
Grown-ups Only!"
-
-
-
-
- DividerOr: "
-
-- or --"
-
-
-
-
- HeaderInstalledServices: "
-
Installed Services"
-
-
- \appservices.html:6 -
-
-
- HeaderAvailableServices: "
-
Available Services"
-
-
- \appservices.html:11 -
-
-
- - diff --git a/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheck.xslt b/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheck.xslt deleted file mode 100644 index 39586022b..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheck.xslt +++ /dev/null @@ -1,145 +0,0 @@ - - - - - - - - - - - - -]> - - - - - - - - <xsl:value-of select="StringUsages/@ReportTitle"/> - - - - -

- -

-
-

Strings

-
- - - - - - - - - - - - - - -
-
: "
-
"
-
:
-
-
- - -
-
\ No newline at end of file diff --git a/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheckSample.xml b/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheckSample.xml deleted file mode 100644 index 9c65bddcd..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheckSample.xml +++ /dev/null @@ -1,222 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MediaBrowser.Tests/ConsistencyTests/StringUsageReporter.cs b/MediaBrowser.Tests/ConsistencyTests/StringUsageReporter.cs deleted file mode 100644 index 1fd511e86..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/StringUsageReporter.cs +++ /dev/null @@ -1,259 +0,0 @@ -using MediaBrowser.Tests.ConsistencyTests.TextIndexing; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Xml; - -namespace MediaBrowser.Tests.ConsistencyTests -{ - /// - /// This class contains tests for reporting the usage of localization string tokens - /// in the dashboard-ui or similar. - /// - /// - /// Run one of the two tests using Visual Studio's "Test Explorer": - /// - /// - /// - /// - /// - /// - /// - /// On successful run, the bottom section of the test explorer will contain a link "Output". - /// This link will open the test results, displaying the trace and two attachment links. - /// One link will open the output folder, the other link will open the output xml file. - /// - /// - /// The output xml file contains a stylesheet link to render the results as html. - /// How that works depends on the default application configured for XML files: - /// - /// - /// Visual Studio - /// Will open in XML source view. To view the html result, click menu - /// 'XML' => 'Start XSLT without debugging' - /// Internet Explorer - /// XSL transform will be applied automatically. - /// Firefox - /// XSL transform will be applied automatically. - /// Chrome - /// Does not work. Chrome is unable/unwilling to apply xslt transforms from local files. - /// - /// - [TestClass] - public class StringUsageReporter - { - /// - /// Root path of the web application - /// - /// - /// Can be an absolute path or a path relative to the binaries folder (bin\Debug). - /// - public const string WebFolder = @"..\..\..\MediaBrowser.WebDashboard\dashboard-ui"; - - /// - /// Path to the strings file, relative to . - /// - public const string StringsFile = @"strings\en-US.json"; - - /// - /// Path to the output folder - /// - /// - /// Can be an absolute path or a path relative to the binaries folder (bin\Debug). - /// Important: When changing the output path, make sure that "StringCheck.xslt" is present - /// to make the XML transform work. - /// - public const string OutputPath = @"."; - - /// - /// List of file extension to search. - /// - public static string[] TargetExtensions = new[] { ".js", ".html" }; - - /// - /// List of paths to exclude from search. - /// - public static string[] ExcludePaths = new[] { @"\bower_components\", @"\thirdparty\" }; - - private TestContext testContextInstance; - - /// - ///Gets or sets the test context which provides - ///information about and functionality for the current test run. - /// - public TestContext TestContext - { - get - { - return testContextInstance; - } - set - { - testContextInstance = value; - } - } - - //[TestMethod] - //public void ReportStringUsage() - //{ - // this.CheckDashboardStrings(false); - //} - - [TestMethod] - public void ReportUnusedStrings() - { - this.CheckDashboardStrings(true); - } - - private void CheckDashboardStrings(Boolean unusedOnly) - { - // Init Folders - var currentDir = System.IO.Directory.GetCurrentDirectory(); - Trace("CurrentDir: {0}", currentDir); - - var rootFolderInfo = ResolveFolder(currentDir, WebFolder); - Trace("Web Root: {0}", rootFolderInfo.FullName); - - var outputFolderInfo = ResolveFolder(currentDir, OutputPath); - Trace("Output Path: {0}", outputFolderInfo.FullName); - - // Load Strings - var stringsFileName = Path.Combine(rootFolderInfo.FullName, StringsFile); - - if (!File.Exists(stringsFileName)) - { - throw new Exception(string.Format("Strings file not found: {0}", stringsFileName)); - } - - int lineNumbers; - var stringsDic = this.CreateStringsDictionary(new FileInfo(stringsFileName), out lineNumbers); - - Trace("Loaded {0} strings from strings file containing {1} lines", stringsDic.Count, lineNumbers); - - var allFiles = rootFolderInfo.GetFiles("*", SearchOption.AllDirectories); - - var filteredFiles1 = allFiles.Where(f => TargetExtensions.Any(e => string.Equals(e, f.Extension, StringComparison.OrdinalIgnoreCase))); - var filteredFiles2 = filteredFiles1.Where(f => !ExcludePaths.Any(p => f.FullName.Contains(p))); - - var selectedFiles = filteredFiles2.OrderBy(f => f.FullName).ToList(); - - var wordIndex = IndexBuilder.BuildIndexFromFiles(selectedFiles, rootFolderInfo.FullName); - - Trace("Created word index from {0} files containing {1} individual words", selectedFiles.Count, wordIndex.Keys.Count); - - var outputFileName = Path.Combine(outputFolderInfo.FullName, string.Format("StringCheck_{0:yyyyMMddHHmmss}.xml", DateTime.Now)); - var settings = new XmlWriterSettings - { - Indent = true, - Encoding = Encoding.UTF8, - WriteEndDocumentOnClose = true - }; - - Trace("Output file: {0}", outputFileName); - - using (XmlWriter writer = XmlWriter.Create(outputFileName, settings)) - { - writer.WriteStartDocument(true); - - // Write the Processing Instruction node. - string xslText = "type=\"text/xsl\" href=\"StringCheck.xslt\""; - writer.WriteProcessingInstruction("xml-stylesheet", xslText); - - writer.WriteStartElement("StringUsages"); - writer.WriteAttributeString("ReportTitle", unusedOnly ? "Unused Strings Report" : "String Usage Report"); - writer.WriteAttributeString("Mode", unusedOnly ? "UnusedOnly" : "All"); - - foreach (var kvp in stringsDic) - { - var occurences = wordIndex.Find(kvp.Key); - - if (occurences == null || !unusedOnly) - { - ////Trace("{0}: {1}", kvp.Key, kvp.Value); - writer.WriteStartElement("Dictionary"); - writer.WriteAttributeString("Token", kvp.Key); - writer.WriteAttributeString("Text", kvp.Value); - - if (occurences != null && !unusedOnly) - { - foreach (var occurence in occurences) - { - writer.WriteStartElement("Occurence"); - writer.WriteAttributeString("FileName", occurence.FileName); - writer.WriteAttributeString("FullPath", occurence.FullPath); - writer.WriteAttributeString("LineNumber", occurence.LineNumber.ToString()); - writer.WriteEndElement(); - ////Trace(" {0}:{1}", occurence.FileName, occurence.LineNumber); - } - } - - writer.WriteEndElement(); - } - } - } - - TestContext.AddResultFile(outputFileName); - TestContext.AddResultFile(outputFolderInfo.FullName); - } - - private SortedDictionary CreateStringsDictionary(FileInfo file, out int lineNumbers) - { - var dic = new SortedDictionary(); - lineNumbers = 0; - - using (var reader = file.OpenText()) - { - while (!reader.EndOfStream) - { - lineNumbers++; - var words = reader - .ReadLine() - .Split(new[] { "\":" }, StringSplitOptions.RemoveEmptyEntries); - - - if (words.Length == 2) - { - var token = words[0].Replace("\"", string.Empty).Trim(); - var text = words[1].Replace("\",", string.Empty).Replace("\"", string.Empty).Trim(); - - if (dic.Keys.Contains(token)) - { - throw new Exception(string.Format("Double string entry found: {0}", token)); - } - - dic.Add(token, text); - } - } - } - - return dic; - } - - private DirectoryInfo ResolveFolder(string currentDir, string folderPath) - { - if (folderPath.IndexOf(@"\:") != 1) - { - folderPath = Path.Combine(currentDir, folderPath); - } - - var folderInfo = new DirectoryInfo(folderPath); - - if (!folderInfo.Exists) - { - throw new Exception(string.Format("Folder not found: {0}", folderInfo.FullName)); - } - - return folderInfo; - } - - - private void Trace(string message, params object[] parameters) - { - var formatted = string.Format(message, parameters); - System.Diagnostics.Trace.WriteLine(formatted); - } - } -} diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/IndexBuilder.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/IndexBuilder.cs deleted file mode 100644 index 4c46f4793..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/IndexBuilder.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing -{ - public class IndexBuilder - { - public const int MinumumWordLength = 4; - - public static char[] WordChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray(); - - public static WordIndex BuildIndexFromFiles(IEnumerable wordFiles, string rootFolderPath) - { - var index = new WordIndex(); - - var wordSeparators = Enumerable.Range(32, 127).Select(e => Convert.ToChar(e)).Where(c => !WordChars.Contains(c)).ToArray(); - wordSeparators = wordSeparators.Concat(new[] { '\t' }).ToArray(); // add tab - - foreach (var file in wordFiles) - { - var lineNumber = 1; - var displayFileName = file.FullName.Replace(rootFolderPath, string.Empty); - using (var reader = file.OpenText()) - { - while (!reader.EndOfStream) - { - var words = reader - .ReadLine() - .Split(wordSeparators, StringSplitOptions.RemoveEmptyEntries); - ////.Select(f => f.Trim()); - - var wordIndex = 1; - foreach (var word in words) - { - if (word.Length >= MinumumWordLength) - { - index.AddWordOccurrence(word, displayFileName, file.FullName, lineNumber, wordIndex++); - } - } - - lineNumber++; - } - } - } - - return index; - } - - } -} diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordIndex.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordIndex.cs deleted file mode 100644 index e0af08792..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordIndex.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing -{ - public class WordIndex : Dictionary - { - public WordIndex() : base(StringComparer.InvariantCultureIgnoreCase) - { - } - - public void AddWordOccurrence(string word, string fileName, string fullPath, int lineNumber, int wordIndex) - { - WordOccurrences current; - if (!this.TryGetValue(word, out current)) - { - current = new WordOccurrences(); - this[word] = current; - } - - current.AddOccurrence(fileName, fullPath, lineNumber, wordIndex); - } - - public WordOccurrences Find(string word) - { - WordOccurrences found; - if (this.TryGetValue(word, out found)) - { - return found; - } - - return null; - } - - } -} diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrence.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrence.cs deleted file mode 100644 index b30e58624..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrence.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing -{ - public struct WordOccurrence - { - public readonly string FileName; // file containing the word. - public readonly string FullPath; // file containing the word. - public readonly int LineNumber; // line within the file. - public readonly int WordIndex; // index within the line. - - public WordOccurrence(string fileName, string fullPath, int lineNumber, int wordIndex) - { - FileName = fileName; - FullPath = fullPath; - LineNumber = lineNumber; - WordIndex = wordIndex; - } - } -} diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrences.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrences.cs deleted file mode 100644 index a6388ab54..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrences.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; - -namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing -{ - public class WordOccurrences : List - { - public void AddOccurrence(string fileName, string fullPath, int lineNumber, int wordIndex) - { - this.Add(new WordOccurrence(fileName, fullPath, lineNumber, wordIndex)); - } - - } -} diff --git a/MediaBrowser.Tests/M3uParserTest.cs b/MediaBrowser.Tests/M3uParserTest.cs deleted file mode 100644 index 583f5f5f0..000000000 --- a/MediaBrowser.Tests/M3uParserTest.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Emby.Server.Implementations.Cryptography; -using Emby.Server.Implementations.LiveTv.TunerHosts; -using MediaBrowser.Common.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace MediaBrowser.Tests -{ - [TestClass] - public class M3uParserTest - { - [TestMethod] - public void TestFormat1() - { - BaseExtensions.CryptographyProvider = new CryptographyProvider(); - - var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:0,84. VOX Schweiz\nhttp://mystream", "-", "-"); - Assert.AreEqual(1, result.Count); - - Assert.AreEqual("VOX Schweiz", result[0].Name); - Assert.AreEqual("84", result[0].Number); - } - [TestMethod] - public void TestFormat2() - { - BaseExtensions.CryptographyProvider = new CryptographyProvider(); - - var input = "#EXTINF:-1 tvg-id=\"\" tvg-name=\"ABC News 04\" tvg-logo=\"\" group-title=\"ABC Group\",ABC News 04"; - input += "\n"; - input += "http://mystream"; - - var result = new M3uParser(new NullLogger(), null, null, null).ParseString(input, "-", "-"); - Assert.AreEqual(1, result.Count); - - Assert.AreEqual("ABC News 04", result[0].Name); - Assert.IsNull(result[0].Number); - } - - [TestMethod] - public void TestFormat3() - { - BaseExtensions.CryptographyProvider = new CryptographyProvider(); - - var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:0, 3.2 - Movies!\nhttp://mystream", "-", "-"); - Assert.AreEqual(1, result.Count); - - Assert.AreEqual("Movies!", result[0].Name); - Assert.AreEqual("3.2", result[0].Number); - } - - [TestMethod] - public void TestFormat4() - { - BaseExtensions.CryptographyProvider = new CryptographyProvider(); - - var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:0 tvg-id=\"abckabclosangeles.path.to\" tvg-logo=\"path.to / channel_logos / abckabclosangeles.png\", ABC KABC Los Angeles\nhttp://mystream", "-", "-"); - Assert.AreEqual(1, result.Count); - - Assert.IsNull(result[0].Number); - Assert.AreEqual("ABC KABC Los Angeles", result[0].Name); - } - - [TestMethod] - public void TestFormat5() - { - BaseExtensions.CryptographyProvider = new CryptographyProvider(); - - var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:-1 channel-id=\"2101\" tvg-id=\"I69387.json.schedulesdirect.org\" group-title=\"Entertainment\",BBC 1 HD\nhttp://mystream", "-", "-"); - Assert.AreEqual(1, result.Count); - - Assert.AreEqual("BBC 1 HD", result[0].Name); - Assert.AreEqual("2101", result[0].Number); - } - - [TestMethod] - public void TestFormat6() - { - BaseExtensions.CryptographyProvider = new CryptographyProvider(); - - var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:-1 tvg-id=\"2101\" group-title=\"Entertainment\",BBC 1 HD\nhttp://mystream", "-", "-"); - Assert.AreEqual(1, result.Count); - - Assert.AreEqual("BBC 1 HD", result[0].Name); - Assert.AreEqual("2101", result[0].Number); - } - } -} diff --git a/MediaBrowser.Tests/MediaBrowser.Tests.csproj b/MediaBrowser.Tests/MediaBrowser.Tests.csproj deleted file mode 100644 index 6415d4211..000000000 --- a/MediaBrowser.Tests/MediaBrowser.Tests.csproj +++ /dev/null @@ -1,139 +0,0 @@ - - - - Debug - AnyCPU - {E22BFD35-0FCD-4A85-978A-C22DCD73A081} - Library - Properties - MediaBrowser.Tests - MediaBrowser.Tests - v4.6.2 - 512 - {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages - False - UnitTest - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\MediaBrowser.Tests.XML - - - none - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\ThirdParty\emby\Emby.Server.MediaEncoding.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {e383961b-9356-4d5d-8233-9a1079d03055} - Emby.Server.Implementations - - - {9142eefa-7570-41e1-bfcc-468bb571af2f} - MediaBrowser.Common - - - {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} - MediaBrowser.Controller - - - {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} - MediaBrowser.Model - - - {442B5058-DCAF-4263-BB6A-F21E31120A1B} - MediaBrowser.Providers - - - {23499896-b135-4527-8574-c26e926ea99e} - MediaBrowser.XbmcMetadata - - - - - - - - - - - - Always - StringCheck.xslt - - - - - - - - - False - - - False - - - False - - - False - - - - - - - - diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/AssParserTests.cs b/MediaBrowser.Tests/MediaEncoding/Subtitles/AssParserTests.cs deleted file mode 100644 index b69faab11..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/AssParserTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Text; -using MediaBrowser.MediaEncoding.Subtitles; -using MediaBrowser.Model.MediaInfo; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using Emby.Server.MediaEncoding.Subtitles; - -namespace MediaBrowser.Tests.MediaEncoding.Subtitles { - - [TestClass] - public class AssParserTests { - - [TestMethod] - public void TestParse() { - - var expectedSubs = - new SubtitleTrackInfo { - TrackEvents = new SubtitleTrackEvent[] { - new SubtitleTrackEvent { - Id = "1", - StartPositionTicks = 24000000, - EndPositionTicks = 72000000, - Text = - "Senator, we're "+ParserValues.NewLine+"making our final "+ParserValues.NewLine+"approach into Coruscant." - }, - new SubtitleTrackEvent { - Id = "2", - StartPositionTicks = 97100000, - EndPositionTicks = 133900000, - Text = - "Very good, Lieutenant." - }, - new SubtitleTrackEvent { - Id = "3", - StartPositionTicks = 150400000, - EndPositionTicks = 180400000, - Text = "It's "+ParserValues.NewLine+"a "+ParserValues.NewLine+"trap!" - } - } - }; - - var sut = new AssParser(); - - var stream = File.OpenRead(@"MediaEncoding\Subtitles\TestSubtitles\data.ass"); - - var result = sut.Parse(stream, CancellationToken.None); - - Assert.IsNotNull(result); - Assert.AreEqual(expectedSubs.TrackEvents.Length,result.TrackEvents.Length); - for (int i = 0; i < expectedSubs.TrackEvents.Length; i++) - { - Assert.AreEqual(expectedSubs.TrackEvents[i].Id, result.TrackEvents[i].Id); - Assert.AreEqual(expectedSubs.TrackEvents[i].StartPositionTicks, result.TrackEvents[i].StartPositionTicks); - Assert.AreEqual(expectedSubs.TrackEvents[i].EndPositionTicks, result.TrackEvents[i].EndPositionTicks); - Assert.AreEqual(expectedSubs.TrackEvents[i].Text, result.TrackEvents[i].Text); - } - - } - - [TestMethod] - public void TestParse2() - { - - var sut = new AssParser(); - - var stream = File.OpenRead(@"MediaEncoding\Subtitles\TestSubtitles\data2.ass"); - - var result = sut.Parse(stream, CancellationToken.None); - - Assert.IsNotNull(result); - - using (var ms = new MemoryStream()) - { - var writer = new SrtWriter(); - writer.Write(result, ms, CancellationToken.None); - - ms.Position = 0; - var text = Encoding.UTF8.GetString(ms.ToArray()); - var b = text; - } - - } - } -} diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs b/MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs deleted file mode 100644 index aae96b382..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using Emby.Server.MediaEncoding.Subtitles; -using Microsoft.Extensions.Logging; -using MediaBrowser.Model.MediaInfo; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace MediaBrowser.Tests.MediaEncoding.Subtitles -{ - - [TestClass] - public class SrtParserTests - { - - [TestMethod] - public void TestParse() - { - - var expectedSubs = - new SubtitleTrackInfo - { - TrackEvents = new SubtitleTrackEvent[] { - new SubtitleTrackEvent { - Id = "1", - StartPositionTicks = 24000000, - EndPositionTicks = 52000000, - Text = - "[Background Music Playing]" - }, - new SubtitleTrackEvent { - Id = "2", - StartPositionTicks = 157120000, - EndPositionTicks = 173990000, - Text = - "Oh my god, Watch out!"+ParserValues.NewLine+"It's coming!!" - }, - new SubtitleTrackEvent { - Id = "3", - StartPositionTicks = 257120000, - EndPositionTicks = 303990000, - Text = "[Bird noises]" - }, - new SubtitleTrackEvent { - Id = "4", - StartPositionTicks = 310000000, - EndPositionTicks = 319990000, - Text = - "This text is RED and has not been positioned." - }, - new SubtitleTrackEvent { - Id = "5", - StartPositionTicks = 320000000, - EndPositionTicks = 329990000, - Text = - "This is a"+ParserValues.NewLine+"new line, as is"+ParserValues.NewLine+"this" - }, - new SubtitleTrackEvent { - Id = "6", - StartPositionTicks = 330000000, - EndPositionTicks = 339990000, - Text = - "This contains nested bold, italic, underline and strike-through HTML tags" - }, - new SubtitleTrackEvent { - Id = "7", - StartPositionTicks = 340000000, - EndPositionTicks = 349990000, - Text = - "Unclosed but supported HTML tags are left in, SSA italics aren't" - }, - new SubtitleTrackEvent { - Id = "8", - StartPositionTicks = 350000000, - EndPositionTicks = 359990000, - Text = - "<ggg>Unsupported</ggg> HTML tags are escaped and left in, even if <hhh>not closed." - }, - new SubtitleTrackEvent { - Id = "9", - StartPositionTicks = 360000000, - EndPositionTicks = 369990000, - Text = - "Multiple SSA tags are stripped" - }, - new SubtitleTrackEvent { - Id = "10", - StartPositionTicks = 370000000, - EndPositionTicks = 379990000, - Text = - "Greater than (<) and less than (>) are shown" - } - } - }; - - var sut = new SrtParser(new NullLogger()); - - var stream = File.OpenRead(@"MediaEncoding\Subtitles\TestSubtitles\unit.srt"); - - var result = sut.Parse(stream, CancellationToken.None); - - Assert.IsNotNull(result); - Assert.AreEqual(expectedSubs.TrackEvents.Length, result.TrackEvents.Length); - for (int i = 0; i < expectedSubs.TrackEvents.Length; i++) - { - Assert.AreEqual(expectedSubs.TrackEvents[i].Id, result.TrackEvents[i].Id); - Assert.AreEqual(expectedSubs.TrackEvents[i].StartPositionTicks, result.TrackEvents[i].StartPositionTicks); - Assert.AreEqual(expectedSubs.TrackEvents[i].EndPositionTicks, result.TrackEvents[i].EndPositionTicks); - Assert.AreEqual(expectedSubs.TrackEvents[i].Text, result.TrackEvents[i].Text); - } - - } - } -} diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data.ass b/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data.ass deleted file mode 100644 index 3114a844a..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data.ass +++ /dev/null @@ -1,23 +0,0 @@ -[Script Info] -Title: Testing subtitles for the SSA Format - -[V4 Styles] -Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding -Style: Default,Arial,20,65535,65535,65535,-2147483640,-1,0,1,3,0,2,30,30,30,0,0 -Style: Titre_episode,Akbar,140,15724527,65535,65535,986895,-1,0,1,1,0,3,30,30,30,0,0 -Style: Wolf main,Wolf_Rain,56,15724527,15724527,15724527,4144959,0,0,1,1,2,2,5,5,30,0,0 - - - -[Events] -Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text -Dialogue: 0,0:00:02.40,0:00:07.20,Default,,0000,0000,0000,,Senator, {\kf89}we're \Nmaking our final \napproach into Coruscant. -Dialogue: 0,0:00:09.71,0:00:13.39,Default,,0000,0000,0000,,{\pos(400,570)}Very good, Lieutenant. -Dialogue: 0,0:00:15.04,0:00:18.04,Default,,0000,0000,0000,,It's \Na \ntrap! - - -[Pictures] -This section will be ignored - -[Fonts] -This section will be ignored \ No newline at end of file diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data2.ass b/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data2.ass deleted file mode 100644 index 98585f636..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data2.ass +++ /dev/null @@ -1,391 +0,0 @@ -[Script Info] -Title: English (US) -ScriptType: v4.00+ -WrapStyle: 0 -PlayResX: 640 -PlayResY: 360 - -[V4+ Styles] -Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding -Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,0010,0010,0010,1 -Style: para-main,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,0020,0020,0015,0 -Style: para-main-top,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H001E0200,&H00000000,0,0,0,0,100,100,0,0,1,2,1,8,0010,0010,0017,0 -Style: para-internal,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H001E0200,&H00000000,0,1,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0 -Style: para-internal-top,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H001E0200,&H00000000,0,1,0,0,100,100,0,0,1,2,1,8,0010,0010,0017,0 -Style: para-overlap,Trebuchet MS,25,&H00BAFCF3,&H000000FF,&H001E0200,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0 -Style: para-narration,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H00000137,&H00000137,0,1,0,0,100,100,0,0,1,2,1,8,0020,0020,0015,0 -Style: para-internaloverlap,Trebuchet MS,25,&H00BAFCF3,&H000000FF,&H001E0200,&H00000000,0,1,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0 -Style: para-flashback,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H004D0000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0 -Style: para-flashbackinternal,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H004D0701,&H00000000,0,1,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0 -Style: para-flashbackoverlap,Trebuchet MS,25,&H00BAFCF3,&H000000FF,&H004D0701,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0 -Style: para-title,arial,35,&H001F00C1,&H000000FF,&H00050058,&H00000137,1,0,0,0,100,100,0,0,1,1,0,7,0050,0020,0050,0 -Style: para-title-maxim,Times New Roman,25,&H00FFF3F3,&H000000FF,&H003B264A,&H00000137,0,0,0,0,100,100,0,0,1,1,0,4,0050,0020,0050,0 -Style: para-ep-title,Times New Roman,25,&H00F8FDFF,&H000000FF,&H005C5C5C,&H00273024,0,0,0,0,100,100,0,0,1,0,1,1,0056,0058,0060,0 -Style: para-next-ep,Trebuchet MS,22,&H009A8D94,&H000000FF,&H00000000,&H00273024,0,0,0,0,100,100,0,0,1,0,0,8,0000,0000,0135,0 -Style: tiny sign,Times New Roman,14,&H002C2F23,&H000000FF,&H00060600,&H00000000,1,0,0,0,100,100,0,0,1,2,0,8,0140,0010,0015,1 -Style: writing1,Verdana,16,&H00292C29,&H000000FF,&H002D241D,&H00000000,0,0,0,0,100,100,0,0,1,0,0,8,0080,0010,0025,1 -Style: writing2,Verdana,12,&H00292C29,&H000000FF,&H002D241D,&H00000000,0,0,0,0,100,100,0,0,1,0,0,3,0080,0090,0085,1 -Style: writing3,Verdana,16,&H00292C29,&H000000FF,&H002D241D,&H00000000,0,0,0,0,100,100,0,0,1,0,0,8,0010,0130,0080,1 -Style: recept,Trebuchet MS,12,&H00AFB2AC,&H000000FF,&H004C4D49,&H00000000,1,0,0,0,100,100,0,0,1,4,0,8,0010,0010,0020,1 -Style: food,Times New Roman,23,&H0056886C,&H000000FF,&H0083E5F9,&H00000000,1,0,0,0,100,100,0,0,1,4,0,7,0020,0010,0070,1 -Style: pad,Times New Roman,12,&H00445F6A,&H000000FF,&H007D6A4F,&H00000000,0,0,0,0,100,100,0,25,1,0,0,2,0040,0010,0105,1 -Style: chalk,Times New Roman,24,&H007B867F,&H000000FF,&H008EE3E9,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0050,0050,0055,1 -Style: fortune,Times New Roman,18,&H00153249,&H000000FF,&H00727FA4,&H00000000,0,0,0,0,100,100,0,0,1,4,0,7,0060,0010,0030,1 -Style: fortune2,Times New Roman,24,&H003277AB,&H000000FF,&H00D0FFFF,&H00000000,1,0,0,0,100,100,0,0,1,4,0,8,0080,0000,0020,1 - -[Events] -Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text - -Dialogue: 0,0:00:06.89,0:00:10.62,para-main,M,0000,0000,0000,,I'm sorry to sour the mood, Shinichi, but... -Dialogue: 0,0:00:10.62,0:00:11.80,para-main,S,0000,0000,0000,,No way. -Dialogue: 0,0:00:11.80,0:00:12.49,para-main,S,0000,0000,0000,,You must be kidding. -Dialogue: 0,0:00:13.00,0:00:14.61,para-main,M,0000,0000,0000,,We need to start running right now. -Dialogue: 0,0:00:15.20,0:00:16.74,para-main,S,0000,0000,0000,,Are you sure it's him? -Dialogue: 0,0:00:17.25,0:00:20.06,para-main,M,0000,0000,0000,,These wavelengths are too \Npowerful to come from some lackey. -Dialogue: 0,0:00:20.06,0:00:22.81,para-main,M,0000,0000,0000,,And given the speed of his \Napproach, I'd say he's in a car. -Dialogue: 0,0:00:23.49,0:00:25.29,para-main,M,0000,0000,0000,,Take a right at that corner. -Dialogue: 0,0:00:25.76,0:00:26.72,para-main,S,0000,0000,0000,,Shit! -Dialogue: 0,0:00:26.72,0:00:28.43,para-main,S,0000,0000,0000,,Now I'm a thief. -Dialogue: 0,0:00:28.43,0:00:30.17,para-main,M,0000,0000,0000,,Is this the time to whine about it? -Dialogue: 0,0:00:31.10,0:00:34.25,para-main,S,0000,0000,0000,,When'd you learn to drive, anyway? -Dialogue: 0,0:00:34.70,0:00:37.80,para-main,M,0000,0000,0000,,I mastered Japanese in \Na single day, you know. -Dialogue: 0,0:00:39.72,0:00:41.46,para-main,S,0000,0000,0000,,Migi, I have a favor to ask. -Dialogue: 0,0:00:42.68,0:00:45.23,para-main,S,0000,0000,0000,,Please go somewhere with \Nas few people as possible. -Dialogue: 0,0:00:45.23,0:00:47.94,para-main,S,0000,0000,0000,,If we fight him in a city, \Nmany people will die. -Dialogue: 0,0:00:49.63,0:00:50.94,para-main,M,0000,0000,0000,,Very well. -Dialogue: 0,0:00:52.20,0:00:55.81,para-main,M,0000,0000,0000,,I've thought of something \Nthat's worth a gamble. -Dialogue: 0,0:01:35.53,0:01:42.66,para-title,,0000,0000,0000,,Parasyte -Dialogue: 0,0:01:37.74,0:01:42.66,para-title-maxim,,0000,0000,0000,,{\fad(2000,1)}The Maxim -Dialogue: 0,0:02:42.01,0:02:46.54,para-ep-title,Sign 0245,0000,0000,0000,,{\fad(350,500)\an3}Quiescence and Awakening -Dialogue: 0,0:02:48.95,0:02:49.69,para-main,S,0000,0000,0000,,Well? -Dialogue: 0,0:02:50.56,0:02:51.55,para-main,M,0000,0000,0000,,It didn't work. -Dialogue: 0,0:02:52.05,0:02:52.99,para-main,M,0000,0000,0000,,He's alive. -Dialogue: 0,0:02:53.89,0:02:55.55,para-main,M,0000,0000,0000,,He's tough. -Dialogue: 0,0:02:56.76,0:02:57.48,para-main,M,0000,0000,0000,,Let's go. -Dialogue: 0,0:02:58.00,0:02:58.82,para-main,S,0000,0000,0000,,Go where? -Dialogue: 0,0:03:00.01,0:03:00.87,para-main,M,0000,0000,0000,,Let's run. -Dialogue: 0,0:03:11.71,0:03:13.13,para-main,M,0000,0000,0000,,All right, stop. -Dialogue: 0,0:03:16.59,0:03:18.09,para-main,S,0000,0000,0000,,Why are we stopping? -Dialogue: 0,0:03:18.09,0:03:20.38,para-main,S,0000,0000,0000,,We can't afford to waste time here! -Dialogue: 0,0:03:20.38,0:03:21.47,para-main,M,0000,0000,0000,,Calm down. -Dialogue: 0,0:03:21.47,0:03:23.97,para-main,M,0000,0000,0000,,Let's strategize until he shows up. -Dialogue: 0,0:03:24.33,0:03:25.85,para-main,S,0000,0000,0000,,Strategize?! -Dialogue: 0,0:03:25.85,0:03:28.21,para-main,S,0000,0000,0000,,We might be minutes away \Nfrom being chopped up! -Dialogue: 0,0:03:28.21,0:03:29.24,para-main,M,0000,0000,0000,,Shinichi. -Dialogue: 0,0:03:29.24,0:03:30.69,para-main,M,0000,0000,0000,,I understand how you feel. -Dialogue: 0,0:03:30.69,0:03:32.48,para-main,M,0000,0000,0000,,Anyone would fear death. -Dialogue: 0,0:03:32.83,0:03:34.48,para-main,M,0000,0000,0000,,I'm afraid, as well. -Dialogue: 0,0:03:34.95,0:03:37.51,para-main,M,0000,0000,0000,,However, this is our moment of truth! -Dialogue: 0,0:03:39.54,0:03:43.25,para-main,M,0000,0000,0000,,You have a strength normal \Nhumans don't have. -Dialogue: 0,0:03:43.25,0:03:46.23,para-main,M,0000,0000,0000,,You can be calm, no matter \Nwhat the circumstance. -Dialogue: 0,0:03:46.85,0:03:48.89,para-main,M,0000,0000,0000,,Now, put your hand on your chest -Dialogue: 0,0:03:48.89,0:03:51.02,para-main,M,0000,0000,0000,,and breathe deeply like you always do. -Dialogue: 0,0:04:00.56,0:04:02.84,para-main,M,0000,0000,0000,,Good, well done. -Dialogue: 0,0:04:03.54,0:04:04.44,para-main,M,0000,0000,0000,,Listen. -Dialogue: 0,0:04:04.44,0:04:08.50,para-main,M,0000,0000,0000,,When it comes to ability, \NGotou surpasses us in every way. -Dialogue: 0,0:04:08.93,0:04:12.85,para-main,M,0000,0000,0000,,By simple calculations, our odds of \Nvictory might be zero percent. -Dialogue: 0,0:04:12.85,0:04:16.22,para-main,M,0000,0000,0000,,But that just means we should approach \Nthis from a different angle. -Dialogue: 0,0:04:16.79,0:04:20.18,para-main,M,0000,0000,0000,,If we can't win even by working together, -Dialogue: 0,0:04:20.85,0:04:23.36,para-main,M,0000,0000,0000,,maybe we should try {\i1}not{\i0} working together. -Dialogue: 0,0:04:23.63,0:04:24.24,para-main,S,0000,0000,0000,,What? -Dialogue: 0,0:04:25.01,0:04:28.12,para-main,M,0000,0000,0000,,In war, what matters is \Nopportunity, not numbers. -Dialogue: 0,0:04:28.12,0:04:29.27,para-main,S,0000,0000,0000,,Opportunity? -Dialogue: 0,0:04:29.86,0:04:33.16,para-main,M,0000,0000,0000,,In your pocket is a lighter I found in the car. -Dialogue: 0,0:04:42.79,0:04:44.01,para-main,Goto,0000,0000,0000,,They're above... -Dialogue: 0,0:04:44.31,0:04:48.01,para-main,Goto,0000,0000,0000,,They've spread out among \Nthe tree branches to hide. -Dialogue: 0,0:04:48.29,0:04:49.89,para-main,Goto,0000,0000,0000,,How unoriginal. -Dialogue: 0,0:04:50.90,0:04:52.68,para-flashbackinternal,M,0000,0000,0000,,This will be a race against time. -Dialogue: 0,0:04:53.27,0:04:57.40,para-flashbackinternal,M,0000,0000,0000,,To a parasite, the body is our lifeline \Nas well as our greatest weakness. -Dialogue: 0,0:04:58.18,0:05:01.39,para-flashbackinternal,M,0000,0000,0000,,My cells that have dispersed in your body -Dialogue: 0,0:05:01.39,0:05:05.21,para-flashbackinternal,M,0000,0000,0000,,have been completely integrated, \Nand are altered, -Dialogue: 0,0:05:05.21,0:05:06.91,para-flashbackinternal,M,0000,0000,0000,,so Gotou can't detect them. -Dialogue: 0,0:05:07.43,0:05:09.72,para-flashbackinternal,M,0000,0000,0000,,He will come straight for me -Dialogue: 0,0:05:09.72,0:05:11.66,para-flashbackinternal,M,0000,0000,0000,,without noticing your presence. -Dialogue: 0,0:05:12.17,0:05:15.50,para-flashbackinternal,M,0000,0000,0000,,If there is a protracted fight, \NI will shrivel up and die. -Dialogue: 0,0:05:15.50,0:05:17.67,para-flashbackinternal,M,0000,0000,0000,,This is an extremely reckless strategy. -Dialogue: 0,0:05:17.67,0:05:21.67,para-flashbackinternal,M,0000,0000,0000,,But that means even Gotou is \Nunlikely to anticipate our strategy. -Dialogue: 0,0:05:39.54,0:05:43.16,para-internal,Gotou,0000,0000,0000,,What, no counterattack? -Dialogue: 0,0:05:46.10,0:05:48.69,para-internal,Gotou,0000,0000,0000,,Where is the human boy? -Dialogue: 0,0:05:48.69,0:05:51.95,para-internal,Gotou,0000,0000,0000,,If only I can find and destroy the body, I'll win. -Dialogue: 0,0:05:53.56,0:05:54.58,para-flashbackinternal,M,0000,0000,0000,,His body -Dialogue: 0,0:05:55.03,0:05:58.58,para-flashbackinternal,M,0000,0000,0000,,is protected by semi-hardened parasite cells. -Dialogue: 0,0:05:59.12,0:06:02.59,para-flashbackinternal,M,0000,0000,0000,,It's unlikely that his entire body is armored, -Dialogue: 0,0:06:02.59,0:06:06.28,para-flashbackinternal,M,0000,0000,0000,,but there's no time to find where \Nthe chinks are in his armor. -Dialogue: 0,0:06:06.28,0:06:08.95,para-flashbackinternal,M,0000,0000,0000,,The part least likely to be armored -Dialogue: 0,0:06:08.95,0:06:12.47,para-flashbackinternal,M,0000,0000,0000,,and thus most suitable as a target... -Dialogue: 0,0:06:13.05,0:06:14.12,para-flashbackinternal,M,0000,0000,0000,,is his head. -Dialogue: 0,0:06:16.41,0:06:19.93,para-flashbackinternal,M,0000,0000,0000,,Unifying the multiple parasites \Nin his torso and limbs must -Dialogue: 0,0:06:19.93,0:06:22.95,para-flashbackinternal,M,0000,0000,0000,,require a tremendous amount of energy. -Dialogue: 0,0:06:22.95,0:06:25.96,para-flashbackinternal,M,0000,0000,0000,,Thus, the "head" has its hands \Nfull acting as the control tower. -Dialogue: 0,0:06:26.53,0:06:29.63,para-flashbackinternal,M,0000,0000,0000,,If we lop the head off, -Dialogue: 0,0:06:29.63,0:06:31.93,para-flashbackinternal,M,0000,0000,0000,,unity will be lost along with his armor, -Dialogue: 0,0:06:31.93,0:06:34.12,para-flashbackinternal,M,0000,0000,0000,,which should allow us to destroy his body. -Dialogue: 0,0:06:34.76,0:06:36.28,para-internal,S,0000,0000,0000,,Any time now, Migi! -Dialogue: 0,0:06:36.74,0:06:39.12,para-internal,S,0000,0000,0000,,If you don't hurry, you'll... -Dialogue: 0,0:06:39.64,0:06:41.33,para-internal,M,0000,0000,0000,,I will only have one chance! -Dialogue: 0,0:06:41.33,0:06:45.88,para-internal,M,0000,0000,0000,,If I am to decapitate Gotou when his \Npower and speed far surpasses my own... -Dialogue: 0,0:06:47.02,0:06:48.51,para-internal,M,0000,0000,0000,,What is this? -Dialogue: 0,0:06:48.51,0:06:50.73,para-internal,M,0000,0000,0000,,My consciousness is already fading... -Dialogue: 0,0:06:51.25,0:06:52.74,para-internal,M,0000,0000,0000,,I must hurry! -Dialogue: 0,0:06:52.74,0:06:54.89,para-internal,M,0000,0000,0000,,But the angle of attack is still poor. -Dialogue: 0,0:06:55.51,0:06:57.09,para-main,Gotou,0000,0000,0000,,Hey! Listen up! -Dialogue: 0,0:06:57.09,0:06:59.12,para-main,Gotou,0000,0000,0000,,Are you that scared of me?! -Dialogue: 0,0:06:59.12,0:07:03.19,para-main,Gotou,0000,0000,0000,,Spreading out in all directions \Nisn't much of a camouflage! -Dialogue: 0,0:07:03.51,0:07:06.69,para-main,Gotou,0000,0000,0000,,Use your brains to fight, not run! -Dialogue: 0,0:07:06.69,0:07:08.11,para-main,M,0000,0000,0000,,Now! Do it! -Dialogue: 0,0:07:08.62,0:07:09.49,para-internal,S,0000,0000,0000,,Was that my voice? -Dialogue: 0,0:07:09.88,0:07:11.24,para-main,G,0000,0000,0000,,There! -Dialogue: 0,0:07:17.41,0:07:20.29,para-flashbackinternal,M,0000,0000,0000,,The surface cells will instinctively disengage -Dialogue: 0,0:07:20.29,0:07:22.58,para-flashbackinternal,M,0000,0000,0000,,from Gotou's command upon exposure to fire, -Dialogue: 0,0:07:22.92,0:07:23.72,para-flashbackinternal,M,0000,0000,0000,,and as a result... -Dialogue: 0,0:07:28.94,0:07:29.67,para-internal,M,0000,0000,0000,,Damn! -Dialogue: 0,0:07:29.67,0:07:30.38,para-internal,M,0000,0000,0000,,Not deep enough! -Dialogue: 0,0:07:34.70,0:07:35.55,para-main,M,0000,0000,0000,,Did we fail? -Dialogue: 0,0:07:35.55,0:07:36.84,para-main,S,0000,0000,0000,,Migi! -Dialogue: 0,0:07:36.84,0:07:38.08,para-main,M,0000,0000,0000,,Stay back, Shinichi! -Dialogue: 0,0:07:39.79,0:07:40.77,para-main,M,0000,0000,0000,,We failed! -Dialogue: 0,0:07:41.31,0:07:43.43,para-main,Gotou,0000,0000,0000,,Well, this is a surprise. -Dialogue: 0,0:07:43.43,0:07:44.31,para-main,Gotou,0000,0000,0000,,Well done. -Dialogue: 0,0:07:44.52,0:07:45.52,para-main,M,0000,0000,0000,,Run! Now! -Dialogue: 0,0:07:45.52,0:07:46.33,para-main,S,0000,0000,0000,,But... -Dialogue: 0,0:07:46.33,0:07:47.36,para-main,M,0000,0000,0000,,Don't come any closer! -Dialogue: 0,0:07:47.61,0:07:48.98,para-main,M,0000,0000,0000,,We don't both need to die! -Dialogue: 0,0:07:52.43,0:07:54.11,para-main,S,0000,0000,0000,,But, Migi... -Dialogue: 0,0:07:59.77,0:08:00.83,para-main,M,0000,0000,0000,,What are you doing?! -Dialogue: 0,0:08:00.83,0:08:01.62,para-main,M,0000,0000,0000,,Hurry up and go! -Dialogue: 0,0:08:10.76,0:08:12.38,para-internal,M,0000,0000,0000,,Goodbye, Shinichi. -Dialogue: 0,0:08:13.06,0:08:15.48,para-internal,M,0000,0000,0000,,This is farewell, Shinichi. -Dialogue: 0,0:08:16.52,0:08:21.88,para-internal,M,0000,0000,0000,,I'm glad I didn't take over \Nyour brain when we first met. -Dialogue: 0,0:08:22.86,0:08:27.24,para-internal,M,0000,0000,0000,,Thanks to that, we made many \Ngood memories as friends... -Dialogue: 0,0:08:33.43,0:08:35.48,para-internal,M,0000,0000,0000,,I'm fading... -Dialogue: 0,0:08:35.91,0:08:37.32,para-internal,M,0000,0000,0000,,I feel oddly sleepy, -Dialogue: 0,0:08:38.47,0:08:42.85,para-internal,M,0000,0000,0000,,yet it's all eclipsed by the \Nfeeling that I'm so alone. -Dialogue: 0,0:08:45.37,0:08:46.44,para-internal,M,0000,0000,0000,,So this... -Dialogue: 0,0:08:47.45,0:08:48.37,para-internal,M,0000,0000,0000,,is death... -Dialogue: 0,0:09:48.98,0:09:49.91,para-main,Mitsu,0000,0000,0000,,Who's there?! -Dialogue: 0,0:09:53.90,0:09:55.91,para-main,S,0000,0000,0000,,Oh, sorry. -Dialogue: 0,0:09:55.91,0:09:57.35,para-main,Mitsu,0000,0000,0000,,A... A burglar?! -Dialogue: 0,0:09:57.35,0:09:58.41,para-main,S,0000,0000,0000,,No! -Dialogue: 0,0:09:59.03,0:10:00.66,para-main,S,0000,0000,0000,,I'm not... But... -Dialogue: 0,0:10:00.66,0:10:02.82,para-main,S,0000,0000,0000,,Sure. You can call me that. -Dialogue: 0,0:10:02.82,0:10:03.70,para-main,Mitsu,0000,0000,0000,,Huh? -Dialogue: 0,0:10:04.52,0:10:07.71,para-main,S,0000,0000,0000,,I did try to drink some water \Nwithout permission, after all. -Dialogue: 0,0:10:07.71,0:10:09.55,para-main,Mitsu,0000,0000,0000,,I see. -Dialogue: 0,0:10:09.55,0:10:11.97,para-main,Mitsu,0000,0000,0000,,Water isn't free, either. -Dialogue: 0,0:10:12.55,0:10:14.19,para-main,S,0000,0000,0000,,S-Sorry... -Dialogue: 0,0:10:14.82,0:10:16.39,para-main,S,0000,0000,0000,,Well, uh... -Dialogue: 0,0:10:16.87,0:10:18.19,para-main,S,0000,0000,0000,,I should go. -Dialogue: 0,0:10:18.19,0:10:19.75,para-main,S,0000,0000,0000,,I'm sorry for the trouble. -Dialogue: 0,0:10:23.35,0:10:24.99,para-main,Mitsu,0000,0000,0000,,Hang on a second. -Dialogue: 0,0:10:24.99,0:10:25.87,para-main,S,0000,0000,0000,,Yes? -Dialogue: 0,0:10:25.87,0:10:27.39,para-main,Mitsu,0000,0000,0000,,You're hurt. -Dialogue: 0,0:10:27.67,0:10:28.31,para-main,S,0000,0000,0000,,Oh... -Dialogue: 0,0:10:28.88,0:10:30.54,para-main,S,0000,0000,0000,,Well, no, uh... -Dialogue: 0,0:10:30.54,0:10:33.07,para-main,Mitsu,0000,0000,0000,,No, your head. -Dialogue: 0,0:10:33.07,0:10:36.03,para-main,Mitsu,0000,0000,0000,,You lost your right arm a long \Ntime ago, from the looks of it. -Dialogue: 0,0:10:37.39,0:10:38.44,para-main,S,0000,0000,0000,,I'm fine. -Dialogue: 0,0:10:38.93,0:10:40.77,para-main,S,0000,0000,0000,,I think the bleeding's already stopped. -Dialogue: 0,0:10:40.77,0:10:43.40,para-main,Mitsu,0000,0000,0000,,Just come in and let me take a look. -Dialogue: 0,0:10:43.81,0:10:44.41,para-main,S,0000,0000,0000,,But... -Dialogue: 0,0:10:44.41,0:10:45.74,para-main,Mitsu,0000,0000,0000,,Hurry up! -Dialogue: 0,0:10:45.74,0:10:48.69,para-main,Mitsu,0000,0000,0000,,A burglar wouldn't be this polite. -Dialogue: 0,0:10:48.69,0:10:51.29,para-main,Mitsu,0000,0000,0000,,Besides, you look like you've been crying. -Dialogue: 0,0:10:54.13,0:10:57.55,para-main,Mitsu,0000,0000,0000,,I worked in retail for a long time. -Dialogue: 0,0:10:57.55,0:11:01.89,para-main,Mitsu,0000,0000,0000,,I can tell a lot about a person from just one look. -Dialogue: 0,0:11:03.49,0:11:09.26,para-main,Mitsu,0000,0000,0000,,This injury wasn't from a fair fight, \Nwas it? You were bullied. -Dialogue: 0,0:11:09.26,0:11:10.39,para-main,S,0000,0000,0000,,Uh... -Dialogue: 0,0:11:11.56,0:11:14.15,para-main,Mitsu,0000,0000,0000,,The cut's pretty deep. -Dialogue: 0,0:11:14.70,0:11:18.14,para-main,Mitsu,0000,0000,0000,,Some people in this world do terrible things. -Dialogue: 0,0:11:30.20,0:11:33.22,para-main,S,0000,0000,0000,,I didn't expect so much kindness \Nfrom a complete stranger. -Dialogue: 0,0:11:34.82,0:11:36.92,para-main,S,0000,0000,0000,,Thank you for everything. -Dialogue: 0,0:11:37.35,0:11:39.76,para-main,Mitsu,0000,0000,0000,,Stay the night. -Dialogue: 0,0:11:39.76,0:11:40.48,para-main,S,0000,0000,0000,,What? -Dialogue: 0,0:11:40.48,0:11:42.34,para-main,S,0000,0000,0000,,I couldn't possibly... -Dialogue: 0,0:11:43.32,0:11:45.73,para-main,Mitsu,0000,0000,0000,,Where do you expect to go this late at night? -Dialogue: 0,0:11:45.73,0:11:48.05,para-main,Mitsu,0000,0000,0000,,There are no hotels around here! -Dialogue: 0,0:11:48.79,0:11:52.32,para-main,S,0000,0000,0000,,Um, do you live here by yourself, Granny? -Dialogue: 0,0:11:52.74,0:11:54.89,para-main,Mitsu,0000,0000,0000,,I don't have any grandchildren your age. -Dialogue: 0,0:11:55.41,0:11:56.91,para-main,S,0000,0000,0000,,Erm, Auntie? -Dialogue: 0,0:11:56.91,0:11:58.40,para-main,Mitsu,0000,0000,0000,,I don't have any nephews, either. -Dialogue: 0,0:11:59.38,0:12:01.40,para-main,Mitsu,0000,0000,0000,,My name is Mitsuyo. -Dialogue: 0,0:12:02.65,0:12:03.80,para-main,S,0000,0000,0000,,I'm sorry. -Dialogue: 0,0:12:04.31,0:12:06.91,para-main,S,0000,0000,0000,,I'm Izumi Shinichi. -Dialogue: 0,0:12:07.72,0:12:09.24,para-main,Mitsu,0000,0000,0000,,Shinichi, eh? -Dialogue: 0,0:12:09.86,0:12:11.22,para-main,Mitsu,0000,0000,0000,,Shin-chan, then. -Dialogue: 0,0:12:13.27,0:12:16.75,para-main,Mitsu,0000,0000,0000,,Sorry for making you help with the shopping. -Dialogue: 0,0:12:16.75,0:12:18.20,para-main,S,0000,0000,0000,,Oh, no problem. -Dialogue: 0,0:12:18.75,0:12:20.10,para-main,S,0000,0000,0000,,I can at least do that much. -Dialogue: 0,0:12:22.76,0:12:26.09,para-main,Mitsu,0000,0000,0000,,Let's just say you're my nephew. -Dialogue: 0,0:12:26.09,0:12:26.84,para-main,S,0000,0000,0000,,Nephew? -Dialogue: 0,0:12:26.84,0:12:29.39,para-main,Mitsu,0000,0000,0000,,Strange things have been \Nhappening around here lately. -Dialogue: 0,0:12:29.39,0:12:31.48,para-main,Mitsu,0000,0000,0000,,They're suspicious of outsiders. -Dialogue: 0,0:12:31.87,0:12:33.17,para-main,S,0000,0000,0000,,Strange things? -Dialogue: 0,0:12:36.59,0:12:38.12,para-main,Mitsu,0000,0000,0000,,This is what I was talking about. -Dialogue: 0,0:12:38.98,0:12:43.54,para-main,Mitsu,0000,0000,0000,,Someone keeps dumping truckloads \Nof garbage without permission. -Dialogue: 0,0:12:43.54,0:12:48.49,para-main,Mitsu,0000,0000,0000,,One time, it caught fire and nearly \Nset the entire mountain ablaze. -Dialogue: 0,0:12:48.85,0:12:52.74,para-main,Mitsu,0000,0000,0000,,I know they say big cities \Nare running out of landfills, -Dialogue: 0,0:12:52.74,0:12:55.11,para-main,Mitsu,0000,0000,0000,,but this is a bit much, don't you think? -Dialogue: 0,0:12:55.11,0:12:55.93,para-main,S,0000,0000,0000,,Yes... -Dialogue: 0,0:12:56.36,0:12:58.27,para-main,Mitsu,0000,0000,0000,,Those of us who live around here -Dialogue: 0,0:12:58.27,0:13:00.84,para-main,Mitsu,0000,0000,0000,,have been keeping watch day and night, -Dialogue: 0,0:13:01.16,0:13:03.50,para-main,Mitsu,0000,0000,0000,,but they're no-shows when we do keep watch -Dialogue: 0,0:13:03.50,0:13:06.49,para-main,Mitsu,0000,0000,0000,,and come the one day we sleep. -Dialogue: 0,0:13:06.49,0:13:09.38,para-internal,S,0000,0000,0000,,Mitsuyo-san had a sharp tongue, -Dialogue: 0,0:13:09.38,0:13:10.86,para-internal,S,0000,0000,0000,,but she was kindhearted. -Dialogue: 0,0:13:11.78,0:13:14.98,para-internal,S,0000,0000,0000,,Whenever I tried to thank her and leave, -Dialogue: 0,0:13:14.98,0:13:18.73,para-internal,S,0000,0000,0000,,she'd stop me with a machine gun \Nbarrage of conversation. -Dialogue: 0,0:13:19.43,0:13:20.86,para-internal,S,0000,0000,0000,,With Migi gone, -Dialogue: 0,0:13:20.86,0:13:23.55,para-internal,S,0000,0000,0000,,I had no idea what to do next. -Dialogue: 0,0:13:23.55,0:13:27.06,para-internal,S,0000,0000,0000,,I ended up staying several days. -Dialogue: 0,0:13:28.68,0:13:31.83,para-internal,S,0000,0000,0000,,But I can't impose on her forever. -Dialogue: 0,0:13:32.36,0:13:36.87,para-internal,S,0000,0000,0000,,I should go home tomorrow \Nand tell Dad everything. -Dialogue: 0,0:13:37.52,0:13:39.30,para-internal,S,0000,0000,0000,,About why I lost my right arm... -Dialogue: 0,0:13:39.93,0:13:42.39,para-internal,S,0000,0000,0000,,About how I had a friend named Migi... -Dialogue: 0,0:13:43.11,0:13:45.54,para-internal,S,0000,0000,0000,,About the day Migi first showed up in my life. -Dialogue: 0,0:13:46.48,0:13:48.39,para-internal,S,0000,0000,0000,,About the days we spent together. -Dialogue: 0,0:13:49.08,0:13:52.73,para-internal,S,0000,0000,0000,,And about how great a guy he was. -Dialogue: 0,0:13:53.94,0:13:55.77,para-internal,S,0000,0000,0000,,To save my life, he... -Dialogue: 0,0:13:55.77,0:13:58.14,para-internal,S,0000,0000,0000,,His intelligence, his courage... -Dialogue: 0,0:13:58.87,0:14:01.43,para-internal,S,0000,0000,0000,,I can't even hope to come \Nclose to him in any way. -Dialogue: 0,0:14:03.01,0:14:06.65,para-internal,S,0000,0000,0000,,He is a true hero! -Dialogue: 0,0:14:11.44,0:14:12.87,para-internal,S,0000,0000,0000,,Where am I? -Dialogue: 0,0:14:12.87,0:14:15.31,para-internal,S,0000,0000,0000,,I think I've been here before. -Dialogue: 0,0:14:16.02,0:14:17.22,para-internal,S,0000,0000,0000,,What's that? -Dialogue: 0,0:14:17.67,0:14:19.70,para-internal,S,0000,0000,0000,,Uh, who're you? -Dialogue: 0,0:14:19.70,0:14:20.63,para-internal,M,0000,0000,0000,,What is it? -Dialogue: 0,0:14:20.63,0:14:22.21,para-internal,M,0000,0000,0000,,Are you looking for something? -Dialogue: 0,0:14:22.21,0:14:24.08,para-internal,S,0000,0000,0000,,Looking? -Dialogue: 0,0:14:24.08,0:14:26.42,para-internal,S,0000,0000,0000,,Yeah, I'm looking for a friend. -Dialogue: 0,0:14:26.42,0:14:27.75,para-internal,M,0000,0000,0000,,A friend? -Dialogue: 0,0:14:28.17,0:14:30.27,para-internal,M,0000,0000,0000,,What does this friend look like? -Dialogue: 0,0:14:30.27,0:14:31.09,para-internal,S,0000,0000,0000,,Look like? -Dialogue: 0,0:14:31.72,0:14:34.12,para-internal,S,0000,0000,0000,,I don't really remember. -Dialogue: 0,0:14:34.12,0:14:35.43,para-internal,M,0000,0000,0000,,Then I can't help you. -Dialogue: 0,0:14:35.43,0:14:37.58,para-internal,S,0000,0000,0000,,Hey, wait. -Dialogue: 0,0:14:37.58,0:14:39.22,para-internal,S,0000,0000,0000,,I think he looked like you... -Dialogue: 0,0:14:39.76,0:14:41.81,para-internal,S,0000,0000,0000,,Right! I remember now! -Dialogue: 0,0:14:41.81,0:14:42.95,para-internal,S,0000,0000,0000,,He... -Dialogue: 0,0:14:42.95,0:14:44.86,para-internal,S,0000,0000,0000,,He died. -Dialogue: 0,0:14:44.86,0:14:46.83,para-internal,M,0000,0000,0000,,What? He's dead? -Dialogue: 0,0:14:46.83,0:14:47.69,para-internal,S,0000,0000,0000,,Yeah... -Dialogue: 0,0:14:48.28,0:14:51.43,para-internal,M,0000,0000,0000,,No, he's alive. -Dialogue: 0,0:14:51.43,0:14:52.07,para-internal,S,0000,0000,0000,,What?! -Dialogue: 0,0:14:52.16,0:14:53.97,para-internal,M,0000,0000,0000,,I can tell. -Dialogue: 0,0:14:53.97,0:14:56.60,para-internal,M,0000,0000,0000,,I actually know his name, too. -Dialogue: 0,0:14:56.60,0:14:58.55,para-internal,S,0000,0000,0000,,His... name? -Dialogue: 0,0:14:58.55,0:15:00.30,para-internal,S,0000,0000,0000,,His name... -Dialogue: 0,0:15:01.27,0:15:02.26,para-main,S,0000,0000,0000,,Migi?! -Dialogue: 0,0:15:07.49,0:15:08.26,para-main,S,0000,0000,0000,,Huh?! -Dialogue: 0,0:15:09.76,0:15:10.46,para-main,S,0000,0000,0000,,M... -Dialogue: 0,0:15:10.88,0:15:11.64,para-main,S,0000,0000,0000,,Migi! -Dialogue: 0,0:15:12.60,0:15:14.76,para-internal,S,0000,0000,0000,,Some of his cells are still here! -Dialogue: 0,0:15:15.68,0:15:17.37,para-main,S,0000,0000,0000,,Hey! It's me! -Dialogue: 0,0:15:17.37,0:15:18.50,para-main,S,0000,0000,0000,,Do you recognize me?! -Dialogue: 0,0:15:18.50,0:15:20.85,para-main,Mitsu,0000,0000,0000,,Keep it down. -Dialogue: 0,0:15:21.30,0:15:23.62,para-main,Mitsu,0000,0000,0000,,Go back to sleep. -Dialogue: 0,0:15:28.12,0:15:29.25,para-internal,S,0000,0000,0000,,It won't work. -Dialogue: 0,0:15:29.74,0:15:31.75,para-internal,S,0000,0000,0000,,Even if he can make a small eye, -Dialogue: 0,0:15:31.75,0:15:34.21,para-internal,S,0000,0000,0000,,it's not enough to be capable \Nof thought or speech. -Dialogue: 0,0:15:36.08,0:15:36.93,para-internal,S,0000,0000,0000,,Migi... -Dialogue: 0,0:15:38.42,0:15:39.24,para-internal,S,0000,0000,0000,,Migi! -Dialogue: 0,0:15:54.23,0:15:55.45,para-main,Mitsu,0000,0000,0000,,I see. -Dialogue: 0,0:15:55.45,0:15:57.42,para-main,Mitsu,0000,0000,0000,,I guess I don't have a choice. -Dialogue: 0,0:15:57.42,0:16:00.75,para-main,Mitsu,0000,0000,0000,,I can't keep you here forever. -Dialogue: 0,0:16:00.75,0:16:03.48,para-main,S,0000,0000,0000,,I don't know how I can ever repay you. -Dialogue: 0,0:16:08.17,0:16:09.58,para-main,Mitsu,0000,0000,0000,,What's the matter? -Dialogue: 0,0:16:09.58,0:16:11.49,para-main,Mitsu,0000,0000,0000,,Why're you here so early in the morning? -Dialogue: 0,0:16:11.49,0:16:13.00,para-main,Taoka,0000,0000,0000,,Hey, you! -Dialogue: 0,0:16:13.00,0:16:15.28,para-main,Taoka,0000,0000,0000,,Are you really Mitsuyo-san's nephew? -Dialogue: 0,0:16:15.89,0:16:18.16,para-main,Mitsu,0000,0000,0000,,What does it matter? -Dialogue: 0,0:16:18.16,0:16:20.54,para-main,Mitsu,0000,0000,0000,,He's about to leave. -Dialogue: 0,0:16:20.54,0:16:22.05,para-main,Taoka,0000,0000,0000,,Not so fast. -Dialogue: 0,0:16:22.05,0:16:24.79,para-main,Taoka,0000,0000,0000,,There are way too many strange \Nthings happening lately. -Dialogue: 0,0:16:24.79,0:16:26.70,para-main,Taoka,0000,0000,0000,,The illegal trash dumping, -Dialogue: 0,0:16:26.70,0:16:29.05,para-main,Taoka,0000,0000,0000,,the car crash between two \Ndriver-less vehicles, -Dialogue: 0,0:16:30.29,0:16:32.30,para-main,Taoka,0000,0000,0000,,and now, murder. -Dialogue: 0,0:16:32.65,0:16:33.88,para-main,Mitsu,0000,0000,0000,,What?! -Dialogue: 0,0:16:34.09,0:16:37.24,para-main,Taoka,0000,0000,0000,,Isn't it always outsiders who commit crimes? -Dialogue: 0,0:16:37.24,0:16:38.85,para-main,Taoka,0000,0000,0000,,Outsiders like him? -Dialogue: 0,0:16:41.62,0:16:45.50,para-main,Nakano,0000,0000,0000,,I told you, that thing was \Nbeyond being an outsider. -Dialogue: 0,0:16:45.50,0:16:47.74,para-main,Mitsu,0000,0000,0000,,He's been with me the entire time— -Dialogue: 0,0:16:47.15,0:16:49.19,para-overlap,Nakano,0000,0000,0000,,It wasn't human! -Dialogue: 0,0:16:49.59,0:16:51.40,para-main,Nakano,0000,0000,0000,,I wasn't just seeing things! -Dialogue: 0,0:16:51.40,0:16:53.24,para-main,Nakano,0000,0000,0000,,It was at least three meters tall! -Dialogue: 0,0:16:53.78,0:16:54.45,para-main,Nakano,0000,0000,0000,,And its legs! -Dialogue: 0,0:16:54.45,0:16:56.70,para-main,Nakano,0000,0000,0000,,Yeah, it had four front legs alone! -Dialogue: 0,0:16:56.98,0:16:59.49,para-main,Nakano,0000,0000,0000,,It had more than three eyes, too! -Dialogue: 0,0:17:00.45,0:17:01.79,para-internal,S,0000,0000,0000,,It's Gotou... -Dialogue: 0,0:17:01.79,0:17:03.20,para-internal,S,0000,0000,0000,,It has to be! -Dialogue: 0,0:17:03.20,0:17:06.23,para-main,Nakano,0000,0000,0000,,Yeah, laugh at me all you want. -Dialogue: 0,0:17:06.23,0:17:09.75,para-main,Nakano,0000,0000,0000,,But see this blood? It's all Kitayama's! -Dialogue: 0,0:17:10.19,0:17:14.85,para-main,Nakano,0000,0000,0000,,Kitayama was killed and eaten \Nby a monster right close by! -Dialogue: 0,0:17:19.80,0:17:22.29,para-main,Det,0000,0000,0000,,So it happened around here? -Dialogue: 0,0:17:22.29,0:17:24.43,para-main,Nakano,0000,0000,0000,,Th-That's right. -Dialogue: 0,0:17:29.90,0:17:31.64,para-main,Naitou,0000,0000,0000,,This is horrible. -Dialogue: 0,0:17:31.64,0:17:34.65,para-main,Cop,0000,0000,0000,,Wait, something similar's happened before... -Dialogue: 0,0:17:35.46,0:17:37.08,para-main,Det,0000,0000,0000,,The Mincemeat Murders? -Dialogue: 0,0:17:38.54,0:17:40.10,para-main,Mitsu,0000,0000,0000,,I see. -Dialogue: 0,0:17:40.10,0:17:42.04,para-main,Mitsu,0000,0000,0000,,Okay, got it. -Dialogue: 0,0:17:46.32,0:17:49.76,para-main,Mitsu,0000,0000,0000,,A bunch of hunters are going \Nto get together tomorrow, -Dialogue: 0,0:17:49.76,0:17:53.05,para-main,Mitsu,0000,0000,0000,,so they should be able to take \Ndown this "monster" then. -Dialogue: 0,0:17:55.04,0:17:58.12,para-main,S,0000,0000,0000,,Hunting rifles won't be \Nenough to take him down. -Dialogue: 0,0:17:58.12,0:17:58.90,para-main,Mitsu,0000,0000,0000,,Huh? -Dialogue: 0,0:17:58.90,0:18:00.05,para-main,Mitsu,0000,0000,0000,,"Him"? -Dialogue: 0,0:18:00.73,0:18:03.31,para-main,Mitsu,0000,0000,0000,,You know the monster? -Dialogue: 0,0:18:04.05,0:18:06.09,para-main,S,0000,0000,0000,,He's here because he's after me. -Dialogue: 0,0:18:06.57,0:18:08.01,para-main,S,0000,0000,0000,,To kill me. -Dialogue: 0,0:18:08.42,0:18:09.39,para-main,Mitsu,0000,0000,0000,,Huh? -Dialogue: 0,0:18:09.39,0:18:10.94,para-main,Mitsu,0000,0000,0000,,Stop talking nonsense. -Dialogue: 0,0:18:11.26,0:18:13.24,para-main,S,0000,0000,0000,,This is my fault! -Dialogue: 0,0:18:13.24,0:18:14.73,para-main,S,0000,0000,0000,,I brought him here! -Dialogue: 0,0:18:14.73,0:18:16.36,para-main,S,0000,0000,0000,,And someone was killed! -Dialogue: 0,0:18:17.15,0:18:20.18,para-main,S,0000,0000,0000,,Many more will die tomorrow \Nif I don't do something! -Dialogue: 0,0:18:21.02,0:18:22.41,para-main,Mitsu,0000,0000,0000,,Shin-chan... -Dialogue: 0,0:18:23.25,0:18:24.87,para-main,S,0000,0000,0000,,I just wanted to keep myself alive. -Dialogue: 0,0:18:25.38,0:18:26.85,para-main,S,0000,0000,0000,,Whatever it took. -Dialogue: 0,0:18:27.54,0:18:30.15,para-main,S,0000,0000,0000,,Friends have died for me, too. -Dialogue: 0,0:18:30.71,0:18:35.20,para-main,S,0000,0000,0000,,But I can't just keep running away on my own. -Dialogue: 0,0:18:36.07,0:18:37.43,para-main,Mitsu,0000,0000,0000,,Why not? -Dialogue: 0,0:18:41.97,0:18:43.90,para-main,Mitsu,0000,0000,0000,,Why not live? -Dialogue: 0,0:18:43.90,0:18:45.81,para-main,Mitsu,0000,0000,0000,,Why not run? -Dialogue: 0,0:18:45.81,0:18:48.98,para-main,Mitsu,0000,0000,0000,,Run if it's only to save your own life. -Dialogue: 0,0:18:49.27,0:18:51.68,para-main,Mitsu,0000,0000,0000,,It's nothing to be ashamed of. -Dialogue: 0,0:18:52.39,0:18:57.15,para-main,S,0000,0000,0000,,Mitsuyo-san, I haven't done \Neverything I can just yet! -Dialogue: 0,0:18:57.80,0:19:00.79,para-main,S,0000,0000,0000,,I have to make use of my life \Nbefore a group of people -Dialogue: 0,0:19:00.79,0:19:03.28,para-main,S,0000,0000,0000,,come face-to-face with \Nthat monster tomorrow! -Dialogue: 0,0:19:04.33,0:19:05.22,para-main,Mitsu,0000,0000,0000,,You idiot! -Dialogue: 0,0:19:05.22,0:19:06.45,para-main,Mitsu,0000,0000,0000,,Cut the bullshit! -Dialogue: 0,0:19:06.45,0:19:08.99,para-main,Mitsu,0000,0000,0000,,Make use of your life? -Dialogue: 0,0:19:08.99,0:19:10.96,para-main,Mitsu,0000,0000,0000,,How dare you speak so \Nlightly of your own life?! -Dialogue: 0,0:19:11.33,0:19:13.14,para-main,Mitsu,0000,0000,0000,,Who do you think you are?! -Dialogue: 0,0:19:13.14,0:19:14.88,para-main,Mitsu,0000,0000,0000,,Use your life? -Dialogue: 0,0:19:14.88,0:19:16.40,para-main,Mitsu,0000,0000,0000,,Don't make me laugh! -Dialogue: 0,0:19:16.40,0:19:19.47,para-main,Mitsu,0000,0000,0000,,What does a snot-nosed brat \Nlike you expect to do?! -Dialogue: 0,0:19:24.99,0:19:27.65,para-main,Mitsu,0000,0000,0000,,Look, I don't know your story, -Dialogue: 0,0:19:27.65,0:19:31.42,para-main,Mitsu,0000,0000,0000,,but you should just let adults \Nhandle this sort of thing. -Dialogue: 0,0:19:40.56,0:19:42.61,para-main,Mitsu,0000,0000,0000,,You're still leaving? -Dialogue: 0,0:19:43.78,0:19:48.70,para-main,Mitsu,0000,0000,0000,,Don't you have someone \Nin your life you care about? -Dialogue: 0,0:19:48.70,0:19:50.98,para-main,Mitsu,0000,0000,0000,,Even if it's a stranger, -Dialogue: 0,0:19:50.98,0:19:53.08,para-main,Mitsu,0000,0000,0000,,once I come to know them, -Dialogue: 0,0:19:53.08,0:19:54.66,para-main,Mitsu,0000,0000,0000,,I can't just abandon them. -Dialogue: 0,0:19:54.66,0:19:57.20,para-main,Mitsu,0000,0000,0000,,That's what it means to be human. -Dialogue: 0,0:19:57.20,0:19:58.73,para-main,Mitsu,0000,0000,0000,,But you... -Dialogue: 0,0:20:00.87,0:20:05.52,para-main,Mitsu,0000,0000,0000,,I don't know how much time you have left, -Dialogue: 0,0:20:05.52,0:20:11.89,para-main,Mitsu,0000,0000,0000,,but give some thought to as many things, \Nas many ideas, as you can come up with. -Dialogue: 0,0:20:12.80,0:20:16.47,para-main,Mitsu,0000,0000,0000,,If you throw everything away, that's the end. -Dialogue: 0,0:20:17.05,0:20:20.10,para-main,Mitsu,0000,0000,0000,,Don't give up, no matter what, -Dialogue: 0,0:20:20.10,0:20:21.78,para-main,Mitsu,0000,0000,0000,,and be flexible. -Dialogue: 0,0:20:31.45,0:20:32.36,para-main,Mitsu,0000,0000,0000,,Wait. -Dialogue: 0,0:20:32.90,0:20:35.81,para-main,Mitsu,0000,0000,0000,,Isn't there something useful \Nyou can take with you? -Dialogue: 0,0:20:35.81,0:20:37.09,para-main,Mitsu,0000,0000,0000,,Like a weapon? -Dialogue: 0,0:20:41.43,0:20:42.84,para-main,S,0000,0000,0000,,This, then. -Dialogue: 0,0:20:42.84,0:20:46.42,para-main,Mitsu,0000,0000,0000,,What, that? It's all rusty. -Dialogue: 0,0:20:46.42,0:20:48.60,para-main,Mitsu,0000,0000,0000,,But I guess it's better than nothing. -Dialogue: 0,0:21:02.69,0:21:03.88,para-internal,Mitsu,0000,0000,0000,,Dear... -Dialogue: 0,0:21:04.62,0:21:06.32,para-internal,Mitsu,0000,0000,0000,,Please keep him safe. -Dialogue: 0,0:21:15.06,0:21:19.51,para-internal,S,0000,0000,0000,,If Gotou's adopted a totally \Ndifferent human appearance, -Dialogue: 0,0:21:19.51,0:21:21.48,para-internal,S,0000,0000,0000,,there'll be no way for me to recognize him. -Dialogue: 0,0:21:21.96,0:21:25.53,para-internal,S,0000,0000,0000,,But I'll cross that bridge when I come to it! -Dialogue: 0,0:22:46.84,0:22:51.03,para-next-ep,Sign 2248,0000,0000,0000,,{\fad(700,1)}Life and Oath -Dialogue: 0,0:22:46.93,0:22:47.67,para-main,S,0000,0000,0000,,Next time: -Dialogue: 0,0:22:48.64,0:22:49.81,para-main,S,0000,0000,0000,,"Life and Oath." diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/expected.vtt b/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/expected.vtt deleted file mode 100644 index b6352e7b5..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/expected.vtt +++ /dev/null @@ -1,32 +0,0 @@ -WEBVTT - -00:00:02.400 --> 00:00:05.200 -[Background Music Playing] - -00:00:15.712 --> 00:00:17.399 -Oh my god, Watch out!
It's coming!! - -00:00:25.712 --> 00:00:30.399 -[Bird noises] - -00:00:31.000 --> 00:00:31.999 -This text is RED and has not been positioned. - -00:00:32.000 --> 00:00:32.999 -This is a
new line, as is
this - -00:00:33.000 --> 00:00:33.999 -This contains nested bold, italic, underline and strike-through HTML tags - -00:00:34.000 --> 00:00:34.999 -Unclosed but supported HTML tags are left in, SSA italics aren't - -00:00:35.000 --> 00:00:35.999 -<ggg>Unsupported</ggg> HTML tags are escaped and left in, even if <hhh>not closed. - -00:00:36.000 --> 00:00:36.999 -Multiple SSA tags are stripped - -00:00:37.000 --> 00:00:37.999 -Greater than (<) and less than (>) are shown - diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/unit.srt b/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/unit.srt deleted file mode 100644 index 1ce811bcb..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/unit.srt +++ /dev/null @@ -1,44 +0,0 @@ - - -1 -00:00:02.400 --> 00:00:05.200 -[Background Music Playing] - -2 -00:00:15,712 --> 00:00:17,399 X1:000 X2:000 Y1:050 Y2:100 -Oh my god, Watch out! -It's coming!! - -3 -00:00:25,712 --> 00:00:30,399 -[Bird noises] - -4 -00:00:31,000 --> 00:00:31,999 -This text is RED and has not been {\pos(142,120)}positioned. - -5 -00:00:32,000 --> 00:00:32,999 -This is a\nnew line, as is\Nthis - -6 -00:00:33,000 --> 00:00:33,999 -This contains nested bold, italic, underline and strike-through HTML tags - -7 -00:00:34,000 --> 00:00:34,999 -Unclosed but supported HTML tags are left in, {\i1} SSA italics aren't - -8 -00:00:35,000 --> 00:00:35,999 -Unsupported HTML tags are escaped and left in, even if not closed. - -9 -00:00:36,000 --> 00:00:36,999 -Multiple {\bord-3.7\clip(1,m 50 0 b 100 0 100 100 50 100 b 0 100 0 0 50 0)\pos(142,120)\t(0,500,\fscx100\fscy100)\b1\c&H000000&}SSA tags are stripped - -10 -00:00:37,000 --> 00:00:37,999 -Greater than (<) and less than (>) are shown - - diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/VttWriterTest.cs b/MediaBrowser.Tests/MediaEncoding/Subtitles/VttWriterTest.cs deleted file mode 100644 index 2d25bcb14..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/VttWriterTest.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using Emby.Server.MediaEncoding.Subtitles; -using MediaBrowser.Model.MediaInfo; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace MediaBrowser.Tests.MediaEncoding.Subtitles { - - [TestClass] - public class VttWriterTest { - [TestMethod] - public void TestWrite() { - var infoSubs = - new SubtitleTrackInfo - { - TrackEvents = new SubtitleTrackEvent[] { - new SubtitleTrackEvent { - Id = "1", - StartPositionTicks = 24000000, - EndPositionTicks = 52000000, - Text = - "[Background Music Playing]" - }, - new SubtitleTrackEvent { - Id = "2", - StartPositionTicks = 157120000, - EndPositionTicks = 173990000, - Text = - "Oh my god, Watch out!
It's coming!!" - }, - new SubtitleTrackEvent { - Id = "3", - StartPositionTicks = 257120000, - EndPositionTicks = 303990000, - Text = "[Bird noises]" - }, - new SubtitleTrackEvent { - Id = "4", - StartPositionTicks = 310000000, - EndPositionTicks = 319990000, - Text = - "This text is RED and has not been positioned." - }, - new SubtitleTrackEvent { - Id = "5", - StartPositionTicks = 320000000, - EndPositionTicks = 329990000, - Text = - "This is a
new line, as is
this" - }, - new SubtitleTrackEvent { - Id = "6", - StartPositionTicks = 330000000, - EndPositionTicks = 339990000, - Text = - "This contains nested bold, italic, underline and strike-through HTML tags" - }, - new SubtitleTrackEvent { - Id = "7", - StartPositionTicks = 340000000, - EndPositionTicks = 349990000, - Text = - "Unclosed but supported HTML tags are left in, SSA italics aren't" - }, - new SubtitleTrackEvent { - Id = "8", - StartPositionTicks = 350000000, - EndPositionTicks = 359990000, - Text = - "<ggg>Unsupported</ggg> HTML tags are escaped and left in, even if <hhh>not closed." - }, - new SubtitleTrackEvent { - Id = "9", - StartPositionTicks = 360000000, - EndPositionTicks = 369990000, - Text = - "Multiple SSA tags are stripped" - }, - new SubtitleTrackEvent { - Id = "10", - StartPositionTicks = 370000000, - EndPositionTicks = 379990000, - Text = - "Greater than (<) and less than (>) are shown" - } - } - }; - - var sut = new VttWriter(); - - if(File.Exists("testVTT.vtt")) - File.Delete("testVTT.vtt"); - using (var file = File.OpenWrite("testVTT.vtt")) - { - sut.Write(infoSubs, file, CancellationToken.None); - } - - var result = File.ReadAllText("testVTT.vtt"); - var expectedText = File.ReadAllText(@"MediaEncoding\Subtitles\TestSubtitles\expected.vtt"); - - Assert.AreEqual(expectedText, result); - } - } -} diff --git a/MediaBrowser.Tests/Properties/AssemblyInfo.cs b/MediaBrowser.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index 1bd3ef5d6..000000000 --- a/MediaBrowser.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Reflection; -using System.Resources; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("MediaBrowser.Tests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin System")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: NeutralResourcesLanguage("en")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/MediaBrowser.Tests/app.config b/MediaBrowser.Tests/app.config deleted file mode 100644 index 5c79b167f..000000000 --- a/MediaBrowser.Tests/app.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/MediaBrowser.sln b/MediaBrowser.sln index 39839e273..d23ca1cdb 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -53,6 +53,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing.Skia", "Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj", "{154872D9-6C12-4007-96E3-8F70A58386CE}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Common.Tests", "tests\Jellyfin.Common.Tests\Jellyfin.Common.Tests.csproj", "{DF194677-DFD3-42AF-9F75-D44D5A416478}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -151,6 +155,10 @@ Global {154872D9-6C12-4007-96E3-8F70A58386CE}.Debug|Any CPU.Build.0 = Debug|Any CPU {154872D9-6C12-4007-96E3-8F70A58386CE}.Release|Any CPU.ActiveCfg = Release|Any CPU {154872D9-6C12-4007-96E3-8F70A58386CE}.Release|Any CPU.Build.0 = Release|Any CPU + {DF194677-DFD3-42AF-9F75-D44D5A416478}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF194677-DFD3-42AF-9F75-D44D5A416478}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF194677-DFD3-42AF-9F75-D44D5A416478}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF194677-DFD3-42AF-9F75-D44D5A416478}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -176,4 +184,7 @@ Global $0.DotNetNamingPolicy = $2 $2.DirectoryNamespaceAssociation = PrefixedHierarchical EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {DF194677-DFD3-42AF-9F75-D44D5A416478} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + EndGlobalSection EndGlobal diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj new file mode 100644 index 000000000..449aaa1a5 --- /dev/null +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp2.2 + + false + + + + + + + + + + + + + diff --git a/tests/Jellyfin.Common.Tests/PasswordHashTests.cs b/tests/Jellyfin.Common.Tests/PasswordHashTests.cs new file mode 100644 index 000000000..5fa86f3bd --- /dev/null +++ b/tests/Jellyfin.Common.Tests/PasswordHashTests.cs @@ -0,0 +1,29 @@ +using MediaBrowser.Common.Cryptography; +using Xunit; +using static MediaBrowser.Common.HexHelper; + +namespace Jellyfin.Common.Tests +{ + public class PasswordHashTests + { + [Theory] + [InlineData("$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D", + "PBKDF2", + "", + "62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] + public void ParseTest(string passwordHash, string id, string salt, string hash) + { + var pass = PasswordHash.Parse(passwordHash); + Assert.Equal(id, pass.Id); + Assert.Equal(salt, ToHexString(pass.Salt)); + Assert.Equal(hash, ToHexString(pass.Hash)); + } + + [Theory] + [InlineData("$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] + public void ToStringTest(string passwordHash) + { + Assert.Equal(passwordHash, PasswordHash.Parse(passwordHash).ToString()); + } + } +} -- cgit v1.2.3 From c9820d30edf1cb8fa99a52ec72b6571d6d4506f7 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Fri, 20 Sep 2019 12:42:08 +0200 Subject: Fix multiple mistakes and warnings --- BDInfo/BDROM.cs | 9 +---- Emby.Server.Implementations/Dto/DtoService.cs | 10 ++--- .../HttpServer/Security/AuthService.cs | 2 +- .../Library/CoreResolutionIgnoreRule.cs | 1 - Emby.Server.Implementations/Library/UserManager.cs | 4 +- .../Library/UserViewManager.cs | 4 +- .../LiveTv/EmbyTV/EncodedRecorder.cs | 3 -- .../LiveTv/Listings/SchedulesDirect.cs | 37 ++++++++---------- .../LiveTv/LiveTvManager.cs | 45 ++++++++++------------ .../TunerHosts/HdHomerun/HdHomerunManager.cs | 4 +- .../Services/StringMapTypeDeserializer.cs | 2 +- Jellyfin.Server/Jellyfin.Server.csproj | 2 +- MediaBrowser.Api/UserLibrary/ItemsService.cs | 4 +- MediaBrowser.Common/Extensions/BaseExtensions.cs | 12 +++--- .../Extensions/CollectionExtensions.cs | 19 ++++++++- MediaBrowser.Controller/Entities/BaseItem.cs | 21 ++++++---- .../MediaEncoding/EncodingHelper.cs | 3 -- MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs | 2 - .../Subtitles/SubtitleEncoder.cs | 8 ++-- .../Savers/EpisodeNfoSaver.cs | 2 +- RSSDP/SsdpDevicePublisher.cs | 4 -- 21 files changed, 94 insertions(+), 104 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/BDInfo/BDROM.cs b/BDInfo/BDROM.cs index 6759ed55a..3a0c14ffd 100644 --- a/BDInfo/BDROM.cs +++ b/BDInfo/BDROM.cs @@ -212,7 +212,6 @@ namespace BDInfo public void Scan() { - var errorStreamClipFiles = new List(); foreach (var streamClipFile in StreamClipFiles.Values) { try @@ -221,7 +220,6 @@ namespace BDInfo } catch (Exception ex) { - errorStreamClipFiles.Add(streamClipFile); if (StreamClipFileScanError != null) { if (StreamClipFileScanError(streamClipFile, ex)) @@ -250,7 +248,6 @@ namespace BDInfo StreamFiles.Values.CopyTo(streamFiles, 0); Array.Sort(streamFiles, CompareStreamFiles); - var errorPlaylistFiles = new List(); foreach (var playlistFile in PlaylistFiles.Values) { try @@ -259,7 +256,6 @@ namespace BDInfo } catch (Exception ex) { - errorPlaylistFiles.Add(playlistFile); if (PlaylistFileScanError != null) { if (PlaylistFileScanError(playlistFile, ex)) @@ -275,7 +271,6 @@ namespace BDInfo } } - var errorStreamFiles = new List(); foreach (var streamFile in streamFiles) { try @@ -296,7 +291,6 @@ namespace BDInfo } catch (Exception ex) { - errorStreamFiles.Add(streamFile); if (StreamFileScanError != null) { if (StreamFileScanError(streamFile, ex)) @@ -431,7 +425,7 @@ namespace BDInfo { return 1; } - else if ((x != null || x.FileInfo != null) && (y == null || y.FileInfo == null)) + else if ((x != null && x.FileInfo != null) && (y == null || y.FileInfo == null)) { return -1; } @@ -451,6 +445,5 @@ namespace BDInfo } } } - } } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 75192a8f1..a3201f0bc 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -218,14 +218,12 @@ namespace Emby.Server.Implementations.Dto AttachUserSpecificInfo(dto, item, user, options); } - if (item is IHasMediaSources hasMediaSources) + if (item is IHasMediaSources + && options.ContainsField(ItemFields.MediaSources)) { - if (options.ContainsField(ItemFields.MediaSources)) - { - dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(item, true, user).ToArray(); + dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(item, true, user).ToArray(); - NormalizeMediaSourceContainers(dto); - } + NormalizeMediaSourceContainers(dto); } if (options.ContainsField(ItemFields.Studios)) diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 3d3f67ca2..93a61fe67 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.HttpServer.Security var user = auth.User; - if (user == null & !auth.UserId.Equals(Guid.Empty)) + if (user == null && auth.UserId != Guid.Empty) { throw new SecurityException("User with Id " + auth.UserId + " not found"); } diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index f1ae2fc9c..8bdb38784 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -57,7 +57,6 @@ namespace Emby.Server.Implementations.Library } var filename = fileInfo.Name; - var path = fileInfo.FullName; // Ignore hidden files on UNIX if (Environment.OSVersion.Platform != PlatformID.Win32NT diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index ac6b4a209..52b2f56ff 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -273,14 +273,12 @@ namespace Emby.Server.Implementations.Library var user = Users.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); var success = false; - string updatedUsername = null; IAuthenticationProvider authenticationProvider = null; if (user != null) { var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false); authenticationProvider = authResult.authenticationProvider; - updatedUsername = authResult.username; success = authResult.success; } else @@ -288,7 +286,7 @@ namespace Emby.Server.Implementations.Library // user is null var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); authenticationProvider = authResult.authenticationProvider; - updatedUsername = authResult.username; + string updatedUsername = authResult.username; success = authResult.success; if (success diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index 4d79cae13..88e2a8fa6 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -236,7 +236,7 @@ namespace Emby.Server.Implementations.Library if (!parentId.Equals(Guid.Empty)) { var parentItem = _libraryManager.GetItemById(parentId); - if (parentItem is Channel parentItemChannel) + if (parentItem is Channel) { return _channelManager.GetLatestChannelItemsInternal( new InternalItemsQuery(user) @@ -248,7 +248,7 @@ namespace Emby.Server.Implementations.Library IncludeItemTypes = request.IncludeItemTypes, EnableTotalRecordCount = false }, - CancellationToken.None).Result.Items; + CancellationToken.None).GetAwaiter().GetResult().Items; } if (parentItem is Folder parent) diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 3cc0541e7..cc9c8e5d2 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -208,9 +208,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private static string GetAudioArgs(MediaSourceInfo mediaSource) { - var mediaStreams = mediaSource.MediaStreams ?? new List(); - var inputAudioCodec = mediaStreams.Where(i => i.Type == MediaStreamType.Audio).Select(i => i.Codec).FirstOrDefault() ?? string.Empty; - return "-codec:a:0 copy"; //var audioChannels = 2; diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index f5dffc22a..9a4c91d0b 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -17,7 +17,6 @@ using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.LiveTv.Listings { @@ -41,6 +40,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings private string UserAgent => _appHost.ApplicationUserAgent; + /// + public string Name => "Schedules Direct"; + + /// + public string Type => nameof(SchedulesDirect); + private static List GetScheduleRequestDates(DateTime startDateUtc, DateTime endDateUtc) { var dates = new List(); @@ -103,7 +108,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings httpOptions.RequestHeaders["token"] = token; using (var response = await Post(httpOptions, true, info).ConfigureAwait(false)) - using (var reader = new StreamReader(response.Content)) { var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync>(response.Content).ConfigureAwait(false); _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId); @@ -122,7 +126,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings httpOptions.RequestContent = "[\"" + string.Join("\", \"", programsID) + "\"]"; using (var innerResponse = await Post(httpOptions, true, info).ConfigureAwait(false)) - using (var innerReader = new StreamReader(innerResponse.Content)) { var programDetails = await _jsonSerializer.DeserializeFromStreamAsync>(innerResponse.Content).ConfigureAwait(false); var programDict = programDetails.ToDictionary(p => p.programID, y => y); @@ -152,14 +155,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase)); var imagesWithoutText = allImages.Where(i => string.Equals(i.text, "no", StringComparison.OrdinalIgnoreCase)); - const double desiredAspect = 0.666666667; + const double DesiredAspect = 2.0 / 3; - programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, desiredAspect) ?? - GetProgramImage(ApiUrl, allImages, true, desiredAspect); + programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ?? + GetProgramImage(ApiUrl, allImages, true, DesiredAspect); - const double wideAspect = 1.77777778; + const double WideAspect = 16.0 / 9; - programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, wideAspect); + programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect); // Don't supply the same image twice if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal)) @@ -167,7 +170,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings programEntry.thumbImage = null; } - programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, wideAspect); + programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect); //programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ?? // GetProgramImage(ApiUrl, data, "Banner-L1", false) ?? @@ -178,6 +181,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID])); } + return programsInfo; } } @@ -185,12 +189,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings private static int GetSizeOrder(ScheduleDirect.ImageData image) { - if (!string.IsNullOrWhiteSpace(image.height)) + if (!string.IsNullOrWhiteSpace(image.height) + && int.TryParse(image.height, out int value)) { - if (int.TryParse(image.height, out int value)) - { - return value; - } + return value; } return 0; @@ -736,16 +738,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings httpOptions.RequestHeaders["token"] = token; - using (var response = await _httpClient.SendAsync(httpOptions, "PUT")) + using (await _httpClient.SendAsync(httpOptions, "PUT")) { } } - public string Name => "Schedules Direct"; - - public static string TypeName = "SchedulesDirect"; - public string Type => TypeName; - private async Task HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(info.ListingsId)) diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index ee975e19a..89b92c999 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -60,16 +60,6 @@ namespace Emby.Server.Implementations.LiveTv private IListingsProvider[] _listingProviders = Array.Empty(); private readonly IFileSystem _fileSystem; - public event EventHandler> SeriesTimerCancelled; - public event EventHandler> TimerCancelled; - public event EventHandler> TimerCreated; - public event EventHandler> SeriesTimerCreated; - - public string GetEmbyTvActiveRecordingPath(string id) - { - return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id); - } - public LiveTvManager( IServerApplicationHost appHost, IServerConfigurationManager config, @@ -102,17 +92,34 @@ namespace Emby.Server.Implementations.LiveTv _tvDtoService = new LiveTvDtoService(dtoService, imageProcessor, loggerFactory, appHost, _libraryManager); } + public event EventHandler> SeriesTimerCancelled; + + public event EventHandler> TimerCancelled; + + public event EventHandler> TimerCreated; + + public event EventHandler> SeriesTimerCreated; + /// /// Gets the services. /// /// The services. public IReadOnlyList Services => _services; + public ITunerHost[] TunerHosts => _tunerHosts; + + public IListingsProvider[] ListingProviders => _listingProviders; + private LiveTvOptions GetConfiguration() { return _config.GetConfiguration("livetv"); } + public string GetEmbyTvActiveRecordingPath(string id) + { + return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id); + } + /// /// Adds the parts. /// @@ -130,13 +137,13 @@ namespace Emby.Server.Implementations.LiveTv { if (service is EmbyTV.EmbyTV embyTv) { - embyTv.TimerCreated += EmbyTv_TimerCreated; - embyTv.TimerCancelled += EmbyTv_TimerCancelled; + embyTv.TimerCreated += OnEmbyTvTimerCreated; + embyTv.TimerCancelled += OnEmbyTvTimerCancelled; } } } - private void EmbyTv_TimerCancelled(object sender, GenericEventArgs e) + private void OnEmbyTvTimerCancelled(object sender, GenericEventArgs e) { var timerId = e.Argument; @@ -149,10 +156,9 @@ namespace Emby.Server.Implementations.LiveTv }); } - private void EmbyTv_TimerCreated(object sender, GenericEventArgs e) + private void OnEmbyTvTimerCreated(object sender, GenericEventArgs e) { var timer = e.Argument; - var service = sender as ILiveTvService; TimerCreated?.Invoke(this, new GenericEventArgs { @@ -164,10 +170,6 @@ namespace Emby.Server.Implementations.LiveTv }); } - public ITunerHost[] TunerHosts => _tunerHosts; - - public IListingsProvider[] ListingProviders => _listingProviders; - public List GetTunerHostTypes() { return _tunerHosts.OrderBy(i => i.Name).Select(i => new NameIdPair @@ -966,9 +968,6 @@ namespace Emby.Server.Implementations.LiveTv private async Task AddRecordingInfo(IEnumerable> programs, CancellationToken cancellationToken) { - var timers = new Dictionary>(); - var seriesTimers = new Dictionary>(); - IReadOnlyList timerList = null; IReadOnlyList seriesTimerList = null; @@ -1601,8 +1600,6 @@ namespace Emby.Server.Implementations.LiveTv if (!string.IsNullOrEmpty(query.Id)) { - var guid = new Guid(query.Id); - timers = timers .Where(i => string.Equals(_tvDtoService.GetInternalTimerId(i.Item1.Id), query.Id, StringComparison.OrdinalIgnoreCase)); } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index 3699b988c..9702392b2 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -424,14 +424,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun return false; } - var nameTag = buf[offset++]; + offset++; // Name Tag var nameLength = buf[offset++]; // skip the name field to get to value for return offset += nameLength; - var valueTag = buf[offset++]; + offset++; // Value Tag var valueLength = buf[offset++]; diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs index c27eb7686..23e22afd5 100644 --- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs +++ b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs @@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Services { PropertySetFn = propertySetFn; PropertyParseStringFn = propertyParseStringFn; - PropertyType = PropertyType; + PropertyType = propertyType; } public Action PropertySetFn { get; private set; } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 35f0c84cb..fa3e9cb35 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -22,7 +22,7 @@ - + diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index ada540ba6..b4a302648 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -455,9 +455,7 @@ namespace MediaBrowser.Api.UserLibrary IncludeItemTypes = new[] { typeof(MusicAlbum).Name }, Name = i, Limit = 1 - - }).Select(albumId => albumId); - + }); }).ToArray(); } diff --git a/MediaBrowser.Common/Extensions/BaseExtensions.cs b/MediaBrowser.Common/Extensions/BaseExtensions.cs index 40c16b957..33473c2be 100644 --- a/MediaBrowser.Common/Extensions/BaseExtensions.cs +++ b/MediaBrowser.Common/Extensions/BaseExtensions.cs @@ -14,20 +14,20 @@ namespace MediaBrowser.Common.Extensions /// Strips the HTML. ///
/// The HTML string. - /// System.String. + /// . public static string StripHtml(this string htmlString) { // http://stackoverflow.com/questions/1349023/how-can-i-strip-html-from-text-in-net - const string pattern = @"<(.|\n)*?>"; + const string Pattern = @"<(.|\n)*?>"; - return Regex.Replace(htmlString, pattern, string.Empty).Trim(); + return Regex.Replace(htmlString, Pattern, string.Empty).Trim(); } /// - /// Gets the M d5. + /// Gets the Md5. /// - /// The STR. - /// Guid. + /// The string. + /// . public static Guid GetMD5(this string str) { using (var provider = MD5.Create()) diff --git a/MediaBrowser.Common/Extensions/CollectionExtensions.cs b/MediaBrowser.Common/Extensions/CollectionExtensions.cs index 3bc0295a0..75b9f59f8 100644 --- a/MediaBrowser.Common/Extensions/CollectionExtensions.cs +++ b/MediaBrowser.Common/Extensions/CollectionExtensions.cs @@ -5,13 +5,28 @@ namespace MediaBrowser.Common.Extensions // The MS CollectionExtensions are only available in netcoreapp public static class CollectionExtensions { - public static TValue GetValueOrDefault (this IReadOnlyDictionary dictionary, TKey key) + public static TValue GetValueOrDefault(this IReadOnlyDictionary dictionary, TKey key) { dictionary.TryGetValue(key, out var ret); return ret; } - // REVIEW: Inline? + /// + /// Copies all the elements of the current collection to the specified list + /// starting at the specified destination array index. The index is specified as a 32-bit integer. + /// + /// The current collection that is the source of the elements. + /// The list that is the destination of the elements copied from the current collection. + /// A 32-bit integer that represents the index in destination at which copying begins. + /// + public static void CopyTo(this IReadOnlyList source, IList destination, int index = 0) + { + for (int i = 0; i < source.Count; i++) + { + destination[index + i] = source[i]; + } + } + /// /// Copies all the elements of the current collection to the specified list /// starting at the specified destination array index. The index is specified as a 32-bit integer. diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 0e9f7ee44..369f63b13 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -2045,7 +2045,7 @@ namespace MediaBrowser.Controller.Entities if (itemByPath == null) { - //Logger.LogWarning("Unable to find linked item at path {0}", info.Path); + Logger.LogWarning("Unable to find linked item at path {0}", info.Path); } return itemByPath; @@ -2057,7 +2057,7 @@ namespace MediaBrowser.Controller.Entities if (item == null) { - //Logger.LogWarning("Unable to find linked item at path {0}", info.Path); + Logger.LogWarning("Unable to find linked item at path {0}", info.Path); } return item; @@ -2085,14 +2085,17 @@ namespace MediaBrowser.Controller.Entities if (!current.Contains(name, StringComparer.OrdinalIgnoreCase)) { - if (current.Length == 0) + int curLen = current.Length; + if (curLen == 0) { Studios = new[] { name }; } else { - var list = - Studios = current.Concat(new[] { name }).ToArray(); + var newArr = new string[curLen + 1]; + current.CopyTo(newArr, 0); + newArr[curLen] = name; + Studios = newArr; } } } @@ -2231,8 +2234,12 @@ namespace MediaBrowser.Controller.Entities else { - var currentCount = ImageInfos.Length; - ImageInfos = ImageInfos.Concat(new[] { image }).ToArray(); + var current = ImageInfos; + var currentCount = current.Length; + var newArr = new ItemImageInfo[currentCount + 1]; + current.CopyTo(newArr, 0); + current[currentCount] = image; + ImageInfos = current; } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 841205d0c..eb3d2ab81 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1252,9 +1252,6 @@ namespace MediaBrowser.Controller.MediaEncoding { if (request.AudioBitRate.HasValue) { - // Make sure we don't request a bitrate higher than the source - var currentBitrate = audioStream == null ? request.AudioBitRate.Value : audioStream.BitRate ?? request.AudioBitRate.Value; - // Don't encode any higher than this return Math.Min(384000, request.AudioBitRate.Value); } diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs index 19009e577..bd727bcdf 100644 --- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs @@ -158,8 +158,6 @@ namespace MediaBrowser.LocalMetadata.Savers /// Task. public static void AddCommonNodes(BaseItem item, XmlWriter writer, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataRepo, IFileSystem fileSystem, IServerConfigurationManager config) { - var writtenProviderIds = new HashSet(StringComparer.OrdinalIgnoreCase); - if (!string.IsNullOrEmpty(item.OfficialRating)) { writer.WriteElementString("ContentRating", item.OfficialRating); diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 9ddfb9b01..d5fa76c3a 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -506,12 +506,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (failed) { - var msg = string.Format("ffmpeg subtitle conversion failed for {Path}", inputPath); + _logger.LogError("ffmpeg subtitle conversion failed for {Path}", inputPath); - _logger.LogError(msg); - - throw new Exception(msg); + throw new Exception( + string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle conversion failed for {0}", inputPath)); } + await SetAssFont(outputPath).ConfigureAwait(false); _logger.LogInformation("ffmpeg subtitle conversion succeeded for {Path}", inputPath); diff --git a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs index aa28fded1..091c1957e 100644 --- a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs @@ -31,7 +31,7 @@ namespace MediaBrowser.XbmcMetadata.Savers /// public override bool IsEnabledFor(BaseItem item, ItemUpdateType updateType) - => !item.SupportsLocalMetadata && item is Episode && updateType >= MinimumUpdateType; + => item.SupportsLocalMetadata && item is Episode && updateType >= MinimumUpdateType; /// protected override void WriteCustomElements(BaseItem item, XmlWriter writer) diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 7f3e56394..53b740052 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -86,7 +86,6 @@ namespace Rssdp.Infrastructure ThrowIfDisposed(); - var minCacheTime = TimeSpan.Zero; bool wasAdded = false; lock (_Devices) { @@ -94,7 +93,6 @@ namespace Rssdp.Infrastructure { _Devices.Add(device); wasAdded = true; - minCacheTime = GetMinimumNonZeroCacheLifetime(); } } @@ -120,14 +118,12 @@ namespace Rssdp.Infrastructure if (device == null) throw new ArgumentNullException(nameof(device)); bool wasRemoved = false; - var minCacheTime = TimeSpan.Zero; lock (_Devices) { if (_Devices.Contains(device)) { _Devices.Remove(device); wasRemoved = true; - minCacheTime = GetMinimumNonZeroCacheLifetime(); } } -- cgit v1.2.3 From fef35d0505c6485954b1eb233a5ea67e33fe1138 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 20 Oct 2019 21:12:03 +0200 Subject: Add clearer exceptions, warnings and docs --- .../Cryptography/CryptographyProvider.cs | 10 +++----- .../Library/DefaultAuthenticationProvider.cs | 5 +++- Emby.Server.Implementations/Library/UserManager.cs | 20 +++++----------- .../Library/UserViewManager.cs | 20 ++++++++-------- .../Session/SessionManager.cs | 27 +++++++++++----------- MediaBrowser.Api/UserLibrary/UserViewsService.cs | 8 ++++++- MediaBrowser.Common/Cryptography/PasswordHash.cs | 6 ++--- MediaBrowser.Controller/Entities/Folder.cs | 10 +++++++- MediaBrowser.Controller/Library/IUserManager.cs | 10 +++++--- MediaBrowser.Model/Cryptography/ICryptoProvider.cs | 4 ---- 10 files changed, 63 insertions(+), 57 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 23b77e268..fec7d161e 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -30,6 +30,9 @@ namespace Emby.Server.Implementations.Cryptography private bool _disposed = false; + /// + /// Initializes a new instance of the class. + /// public CryptographyProvider() { // FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto @@ -59,12 +62,6 @@ namespace Emby.Server.Implementations.Cryptography throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}"); } - public byte[] ComputeHash(string hashMethod, byte[] bytes) - => ComputeHash(hashMethod, bytes, Array.Empty()); - - public byte[] ComputeHashWithDefaultMethod(byte[] bytes) - => ComputeHash(DefaultHashMethod, bytes); - public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt) { if (hashMethod == DefaultHashMethod) @@ -90,7 +87,6 @@ namespace Emby.Server.Implementations.Cryptography } throw new CryptographicException($"Requested hash method is not supported: {hashMethod}"); - } public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt) diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index c95b00ede..a1143471d 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -59,7 +59,10 @@ namespace Emby.Server.Implementations.Library if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id) || _cryptographyProvider.DefaultHashMethod == readyHash.Id) { - byte[] calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.Salt); + byte[] calculatedHash = _cryptographyProvider.ComputeHash( + readyHash.Id, + passwordbytes, + readyHash.Salt); if (calculatedHash.SequenceEqual(readyHash.Hash)) { diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 52b2f56ff..3d1030d4b 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -179,12 +179,7 @@ namespace Emby.Server.Implementations.Library _defaultPasswordResetProvider = passwordResetProviders.OfType().First(); } - /// - /// Gets a User by Id. - /// - /// The id. - /// User. - /// + /// public User GetUserById(Guid id) { if (id == Guid.Empty) @@ -196,11 +191,7 @@ namespace Emby.Server.Implementations.Library return user; } - /// - /// Gets the user by identifier. - /// - /// The identifier. - /// User. + /// public User GetUserById(string id) => GetUserById(new Guid(id)); @@ -428,7 +419,6 @@ namespace Emby.Server.Implementations.Library { try { - var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false) : await provider.Authenticate(username, password).ConfigureAwait(false); @@ -538,6 +528,8 @@ namespace Emby.Server.Implementations.Library defaultName = "MyJellyfinUser"; } + _logger.LogWarning("No users, creating one with username {UserName}", defaultName); + var name = MakeValidUsername(defaultName); var user = InstantiateNewUser(name); @@ -601,7 +593,7 @@ namespace Emby.Server.Implementations.Library catch (Exception ex) { // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions - _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {user}", user.Name); + _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {User}", user.Name); } } @@ -625,7 +617,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Error getting {imageType} image info for {imagePath}", image.Type, image.Path); + _logger.LogError(ex, "Error getting {ImageType} image info for {ImagePath}", image.Type, image.Path); return null; } } diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index 88e2a8fa6..474884e42 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -42,6 +42,11 @@ namespace Emby.Server.Implementations.Library { var user = _userManager.GetUserById(query.UserId); + if (user == null) + { + throw new ArgumentException("User Id does not exists.", nameof(query)); + } + var folders = _libraryManager.GetUserRootFolder() .GetChildren(user, true) .OfType() @@ -54,7 +59,7 @@ namespace Emby.Server.Implementations.Library foreach (var folder in folders) { var collectionFolder = folder as ICollectionFolder; - var folderViewType = collectionFolder == null ? null : collectionFolder.CollectionType; + var folderViewType = collectionFolder?.CollectionType; if (UserView.IsUserSpecific(folder)) { @@ -130,16 +135,11 @@ namespace Emby.Server.Implementations.Library { var index = orders.IndexOf(i.Id.ToString("N", CultureInfo.InvariantCulture)); - if (index == -1) + if (index == -1 + && i is UserView view + && view.DisplayParentId != Guid.Empty) { - var view = i as UserView; - if (view != null) - { - if (!view.DisplayParentId.Equals(Guid.Empty)) - { - index = orders.IndexOf(view.DisplayParentId.ToString("N", CultureInfo.InvariantCulture)); - } - } + index = orders.IndexOf(view.DisplayParentId.ToString("N", CultureInfo.InvariantCulture)); } return index == -1 ? int.MaxValue : index; diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 61329160a..d1392e162 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1388,27 +1388,28 @@ namespace Emby.Server.Implementations.Session if (user != null) { // TODO: Move this to userManager? - if (!string.IsNullOrEmpty(request.DeviceId)) + if (!string.IsNullOrEmpty(request.DeviceId) + && !_deviceManager.CanAccessDevice(user, request.DeviceId)) { - if (!_deviceManager.CanAccessDevice(user, request.DeviceId)) - { - throw new SecurityException("User is not allowed access from this device."); - } + throw new SecurityException("User is not allowed access from this device."); } } if (enforcePassword) { - var result = await _userManager.AuthenticateUser(request.Username, request.Password, request.PasswordSha1, request.RemoteEndPoint, true).ConfigureAwait(false); - - if (result == null) - { - AuthenticationFailed?.Invoke(this, new GenericEventArgs(request)); + user = await _userManager.AuthenticateUser( + request.Username, + request.Password, + request.PasswordSha1, + request.RemoteEndPoint, + true).ConfigureAwait(false); + } - throw new SecurityException("Invalid user or password entered."); - } + if (user == null) + { + AuthenticationFailed?.Invoke(this, new GenericEventArgs(request)); - user = result; + throw new SecurityException("Invalid user or password entered."); } var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName); diff --git a/MediaBrowser.Api/UserLibrary/UserViewsService.cs b/MediaBrowser.Api/UserLibrary/UserViewsService.cs index 2fa5d8933..d62049ce9 100644 --- a/MediaBrowser.Api/UserLibrary/UserViewsService.cs +++ b/MediaBrowser.Api/UserLibrary/UserViewsService.cs @@ -10,6 +10,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Services; +using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.UserLibrary { @@ -49,7 +50,12 @@ namespace MediaBrowser.Api.UserLibrary private readonly IAuthorizationContext _authContext; private readonly ILibraryManager _libraryManager; - public UserViewsService(IUserManager userManager, IUserViewManager userViewManager, IDtoService dtoService, IAuthorizationContext authContext, ILibraryManager libraryManager) + public UserViewsService( + IUserManager userManager, + IUserViewManager userViewManager, + IDtoService dtoService, + IAuthorizationContext authContext, + ILibraryManager libraryManager) { _userManager = userManager; _userViewManager = userViewManager; diff --git a/MediaBrowser.Common/Cryptography/PasswordHash.cs b/MediaBrowser.Common/Cryptography/PasswordHash.cs index 7741571db..1cb70675c 100644 --- a/MediaBrowser.Common/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Common/Cryptography/PasswordHash.cs @@ -61,13 +61,13 @@ namespace MediaBrowser.Common.Cryptography /// Return the hashed password. public byte[] Hash { get; } - public static PasswordHash Parse(string storageString) + public static PasswordHash Parse(string hashString) { - string[] splitted = storageString.Split('$'); + string[] splitted = hashString.Split('$'); // The string should at least contain the hash function and the hash itself if (splitted.Length < 3) { - throw new ArgumentException("String doesn't contain enough segments", nameof(storageString)); + throw new ArgumentException("String doesn't contain enough segments", nameof(hashString)); } // Start at 1, the first index shouldn't contain any data diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index d61a07066..b697f6576 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1152,6 +1152,11 @@ namespace MediaBrowser.Controller.Entities public List GetChildren(User user, bool includeLinkedChildren) { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return GetChildren(user, includeLinkedChildren, null); } @@ -1163,7 +1168,10 @@ namespace MediaBrowser.Controller.Entities } //the true root should return our users root folder children - if (IsPhysicalRoot) return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren); + if (IsPhysicalRoot) + { + return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren); + } var result = new Dictionary(); diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index bbedc0442..6163c0ce6 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -39,17 +39,21 @@ namespace MediaBrowser.Controller.Library event EventHandler> UserDeleted; event EventHandler> UserCreated; + event EventHandler> UserPolicyUpdated; + event EventHandler> UserConfigurationUpdated; + event EventHandler> UserPasswordChanged; + event EventHandler> UserLockedOut; /// - /// Gets a User by Id + /// Gets a User by Id. /// /// The id. - /// User. - /// + /// The user with the specified Id, or null id the user doesn't exist. + /// id is an empty Guid. User GetUserById(Guid id); /// diff --git a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs index ce6493232..2d75c9b3e 100644 --- a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs +++ b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs @@ -8,10 +8,6 @@ namespace MediaBrowser.Model.Cryptography IEnumerable GetSupportedHashMethods(); - byte[] ComputeHash(string HashMethod, byte[] bytes); - - byte[] ComputeHashWithDefaultMethod(byte[] bytes); - byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt); byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt); -- cgit v1.2.3 From d9a03c9bb120cada54729d314a204a63fbf607b5 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 10 Sep 2019 22:37:53 +0200 Subject: Fix more warnings --- .../Channels/ChannelManager.cs | 4 +- .../Collections/CollectionManager.cs | 10 +-- .../EntryPoints/RefreshUsersMetadata.cs | 2 +- .../Library/LibraryManager.cs | 16 ++--- .../Library/MediaSourceManager.cs | 13 ++-- Emby.Server.Implementations/Library/UserManager.cs | 2 +- .../Library/Validators/PeopleValidator.cs | 28 +++++--- .../LiveTv/EmbyTV/EmbyTV.cs | 20 +++--- .../LiveTv/LiveTvManager.cs | 29 ++++---- .../Playlists/PlaylistManager.cs | 29 ++++---- .../ScheduledTasks/Tasks/ChapterImagesTask.cs | 15 +++-- .../ServerApplicationPaths.cs | 2 +- MediaBrowser.Api/IHasItemFields.cs | 3 +- MediaBrowser.Api/ItemLookupService.cs | 31 ++++----- MediaBrowser.Api/ItemRefreshService.cs | 2 +- MediaBrowser.Api/ItemUpdateService.cs | 16 +++-- MediaBrowser.Api/Subtitles/SubtitleService.cs | 3 +- MediaBrowser.Api/UserLibrary/UserLibraryService.cs | 2 +- .../Entities/AggregateFolder.cs | 2 +- MediaBrowser.Controller/Entities/BaseItem.cs | 4 +- .../Entities/CollectionFolder.cs | 2 +- MediaBrowser.Controller/Entities/Folder.cs | 2 +- MediaBrowser.Controller/Entities/User.cs | 2 +- .../Providers/DirectoryService.cs | 9 +-- .../Subtitles/ISubtitleManager.cs | 10 +-- MediaBrowser.Model/IO/StreamDefaults.cs | 6 +- .../Books/AudioBookMetadataService.cs | 3 +- .../Books/BookMetadataService.cs | 15 +++-- .../BoxSets/BoxSetMetadataService.cs | 37 ++++++---- .../Channels/ChannelMetadataService.cs | 13 +++- .../Folders/CollectionFolderMetadataService.cs | 13 +++- .../Folders/FolderMetadataService.cs | 16 +++-- .../Folders/UserViewMetadataService.cs | 13 +++- .../Genres/GenreMetadataService.cs | 13 +++- .../LiveTv/ProgramMetadataService.cs | 13 +++- .../Manager/ItemImageProvider.cs | 2 +- MediaBrowser.Providers/Manager/MetadataService.cs | 6 +- MediaBrowser.Providers/Manager/ProviderManager.cs | 4 +- MediaBrowser.Providers/Movies/MovieExternalIds.cs | 17 +++-- .../Movies/MovieMetadataService.cs | 48 ++++--------- .../Movies/TrailerMetadataService.cs | 49 ++++++++++++++ .../Music/AlbumMetadataService.cs | 22 +++--- .../Music/ArtistMetadataService.cs | 31 ++++++--- .../Music/AudioDbAlbumImageProvider.cs | 20 +++--- .../Music/AudioDbAlbumProvider.cs | 45 +++++++------ .../Music/AudioDbArtistImageProvider.cs | 19 +++--- .../Music/AudioDbArtistProvider.cs | 65 +++++++++--------- MediaBrowser.Providers/Music/AudioDbExternalIds.cs | 35 +++++----- .../Music/AudioMetadataService.cs | 3 +- .../Music/MusicBrainzAlbumProvider.cs | 78 +++++++++++++--------- .../Music/MusicBrainzArtistProvider.cs | 27 +++----- MediaBrowser.Providers/Music/MusicExternalIds.cs | 60 ++++++++++------- .../Music/MusicVideoMetadataService.cs | 3 +- .../MusicGenres/MusicGenreMetadataService.cs | 13 +++- .../People/PersonMetadataService.cs | 13 +++- .../People/TvdbPersonImageProvider.cs | 22 +++--- .../Photos/PhotoAlbumMetadataService.cs | 13 +++- .../Photos/PhotoMetadataService.cs | 13 +++- .../Playlists/PlaylistMetadataService.cs | 33 +++++---- .../Studios/StudioMetadataService.cs | 12 +++- .../Subtitles/SubtitleManager.cs | 75 +++++++++++---------- MediaBrowser.Providers/TV/DummySeasonProvider.cs | 27 +++++--- .../TV/EpisodeMetadataService.cs | 16 +++-- .../TV/MissingEpisodeProvider.cs | 30 ++++----- MediaBrowser.Providers/TV/SeasonMetadataService.cs | 23 +++++-- MediaBrowser.Providers/TV/SeriesMetadataService.cs | 11 +-- MediaBrowser.Providers/TV/TvExternalIds.cs | 37 +++++----- .../Users/UserMetadataService.cs | 13 +++- .../Videos/VideoMetadataService.cs | 16 +++-- .../Years/YearMetadataService.cs | 13 +++- 70 files changed, 767 insertions(+), 547 deletions(-) create mode 100644 MediaBrowser.Providers/Movies/TrailerMetadataService.cs (limited to 'Emby.Server.Implementations/Library/UserManager.cs') diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 8e5f5b561..22681fb49 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -470,7 +470,7 @@ namespace Emby.Server.Implementations.Channels _libraryManager.CreateItem(item, null); } - await item.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + await item.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = !isNew && forceUpdate }, cancellationToken); @@ -1156,7 +1156,7 @@ namespace Emby.Server.Implementations.Channels if (isNew || forceUpdate || item.DateLastRefreshed == default(DateTime)) { - _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), RefreshPriority.Normal); + _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal); } return item; diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index bb5057b1c..6d414be73 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -149,7 +149,7 @@ namespace Emby.Server.Implementations.Collections if (options.ItemIdList.Length > 0) { - AddToCollection(collection.Id, options.ItemIdList, false, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + AddToCollection(collection.Id, options.ItemIdList, false, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { // The initial adding of items is going to create a local metadata file // This will cause internet metadata to be skipped as a result @@ -158,7 +158,7 @@ namespace Emby.Server.Implementations.Collections } else { - _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), RefreshPriority.High); + _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); } CollectionCreated?.Invoke(this, new CollectionCreatedEventArgs @@ -178,12 +178,12 @@ namespace Emby.Server.Implementations.Collections public void AddToCollection(Guid collectionId, IEnumerable ids) { - AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))); + AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem))); } public void AddToCollection(Guid collectionId, IEnumerable ids) { - AddToCollection(collectionId, ids.Select(i => i.ToString("N", CultureInfo.InvariantCulture)), true, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))); + AddToCollection(collectionId, ids.Select(i => i.ToString("N", CultureInfo.InvariantCulture)), true, new MetadataRefreshOptions(new DirectoryService(_fileSystem))); } private void AddToCollection(Guid collectionId, IEnumerable ids, bool fireEvent, MetadataRefreshOptions refreshOptions) @@ -287,7 +287,7 @@ namespace Emby.Server.Implementations.Collections } collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); - _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, RefreshPriority.High); diff --git a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs index b2328121e..3a7516dca 100644 --- a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs +++ b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs @@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.EntryPoints { cancellationToken.ThrowIfCancellationRequested(); - await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), cancellationToken).ConfigureAwait(false); + await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false); } } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 87e951f25..13857c1e8 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -519,7 +519,7 @@ namespace Emby.Server.Implementations.Library } public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null) - => ResolvePath(fileInfo, new DirectoryService(_logger, _fileSystem), null, parent); + => ResolvePath(fileInfo, new DirectoryService(_fileSystem), null, parent); private BaseItem ResolvePath( FileSystemMetadata fileInfo, @@ -1045,7 +1045,7 @@ namespace Emby.Server.Implementations.Library await RootFolder.ValidateChildren( new SimpleProgress(), cancellationToken, - new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), + new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: false).ConfigureAwait(false); await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false); @@ -1053,7 +1053,7 @@ namespace Emby.Server.Implementations.Library await GetUserRootFolder().ValidateChildren( new SimpleProgress(), cancellationToken, - new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), + new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: false).ConfigureAwait(false); // Quickly scan CollectionFolders for changes @@ -1074,7 +1074,7 @@ namespace Emby.Server.Implementations.Library innerProgress.RegisterAction(pct => progress.Report(pct * .96)); // Now validate the entire media library - await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), recursive: true).ConfigureAwait(false); + await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true).ConfigureAwait(false); progress.Report(96); @@ -2135,7 +2135,7 @@ namespace Emby.Server.Implementations.Library if (refresh) { item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); - _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), RefreshPriority.Normal); + _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal); } return item; @@ -2193,7 +2193,7 @@ namespace Emby.Server.Implementations.Library { _providerManagerFactory().QueueRefresh( item.Id, - new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { // Need to force save to increment DateLastSaved ForceSave = true @@ -2261,7 +2261,7 @@ namespace Emby.Server.Implementations.Library { _providerManagerFactory().QueueRefresh( item.Id, - new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { // Need to force save to increment DateLastSaved ForceSave = true @@ -2338,7 +2338,7 @@ namespace Emby.Server.Implementations.Library { _providerManagerFactory().QueueRefresh( item.Id, - new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { // Need to force save to increment DateLastSaved ForceSave = true diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index d83e1fc02..7a26e0c37 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -134,12 +134,13 @@ namespace Emby.Server.Implementations.Library if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio || i.Type == MediaStreamType.Video)) { - await item.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) - { - EnableRemoteContentProbe = true, - MetadataRefreshMode = MediaBrowser.Controller.Providers.MetadataRefreshMode.FullRefresh - - }, cancellationToken).ConfigureAwait(false); + await item.RefreshMetadata( + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + EnableRemoteContentProbe = true, + MetadataRefreshMode = MetadataRefreshMode.FullRefresh + }, + cancellationToken).ConfigureAwait(false); mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user); } diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 52b2f56ff..2b6ae1297 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -639,7 +639,7 @@ namespace Emby.Server.Implementations.Library { foreach (var user in Users) { - await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), cancellationToken).ConfigureAwait(false); + await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false); } } diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs index d00c6cde1..137a010ec 100644 --- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -11,16 +11,17 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library.Validators { /// - /// Class PeopleValidator + /// Class PeopleValidator. /// public class PeopleValidator { /// - /// The _library manager + /// The _library manager. /// private readonly ILibraryManager _libraryManager; + /// - /// The _logger + /// The _logger. /// private readonly ILogger _logger; @@ -62,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Validators { var item = _libraryManager.GetPerson(person); - var options = new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ImageRefreshMode = MetadataRefreshMode.ValidationOnly, MetadataRefreshMode = MetadataRefreshMode.ValidationOnly @@ -96,12 +97,19 @@ namespace Emby.Server.Implementations.Library.Validators foreach (var item in deadEntities) { - _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name); - - _libraryManager.DeleteItem(item, new DeleteOptions - { - DeleteFileLocation = false - }, false); + _logger.LogInformation( + "Deleting dead {2} {0} {1}.", + item.Id.ToString("N", CultureInfo.InvariantCulture), + item.Name, + item.GetType().Name); + + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false + }, + false); } progress.Report(100); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index da0013f12..687a178a6 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -1489,16 +1489,18 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { _logger.LogInformation("Refreshing recording parent {path}", item.Path); - _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) - { - RefreshPaths = new string[] + _providerManager.QueueRefresh( + item.Id, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { - path, - Path.GetDirectoryName(path), - Path.GetDirectoryName(Path.GetDirectoryName(path)) - } - - }, RefreshPriority.High); + RefreshPaths = new string[] + { + path, + Path.GetDirectoryName(path), + Path.GetDirectoryName(Path.GetDirectoryName(path)) + } + }, + RefreshPriority.High); } } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 89b92c999..49308b2b1 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -1226,12 +1226,13 @@ namespace Emby.Server.Implementations.LiveTv currentChannel.AddTag("Kids"); } - //currentChannel.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken); - await currentChannel.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) - { - ForceSave = true - - }, cancellationToken).ConfigureAwait(false); + currentChannel.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken); + await currentChannel.RefreshMetadata( + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + ForceSave = true + }, + cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -1245,7 +1246,7 @@ namespace Emby.Server.Implementations.LiveTv numComplete++; double percent = numComplete / (double)allChannelsList.Count; - progress.Report(85 * percent + 15); + progress.Report((85 * percent) + 15); } progress.Report(100); @@ -1278,12 +1279,14 @@ namespace Emby.Server.Implementations.LiveTv if (item != null) { - _libraryManager.DeleteItem(item, new DeleteOptions - { - DeleteFileLocation = false, - DeleteFromExternalProvider = false - - }, false); + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false, + DeleteFromExternalProvider = false + }, + false); } } diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 40b568b40..0f58e43ed 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -90,8 +90,7 @@ namespace Emby.Server.Implementations.Playlists } else { - var folder = item as Folder; - if (folder != null) + if (item is Folder folder) { options.MediaType = folder.GetRecursiveChildren(i => !i.IsFolder && i.SupportsAddingToPlaylist) .Select(i => i.MediaType) @@ -140,7 +139,7 @@ namespace Emby.Server.Implementations.Playlists parentFolder.AddChild(playlist, CancellationToken.None); - await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) { ForceSave = true }, CancellationToken.None) + await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None) .ConfigureAwait(false); if (options.ItemIdList.Length > 0) @@ -201,7 +200,7 @@ namespace Emby.Server.Implementations.Playlists var list = new List(); - var items = (GetPlaylistItems(itemIds, playlist.MediaType, user, options)) + var items = GetPlaylistItems(itemIds, playlist.MediaType, user, options) .Where(i => i.SupportsAddingToPlaylist) .ToList(); @@ -221,18 +220,18 @@ namespace Emby.Server.Implementations.Playlists SavePlaylistFile(playlist); } - _providerManager.QueueRefresh(playlist.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) - { - ForceSave = true - - }, RefreshPriority.High); + _providerManager.QueueRefresh( + playlist.Id, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + ForceSave = true + }, + RefreshPriority.High); } public void RemoveFromPlaylist(string playlistId, IEnumerable entryIds) { - var playlist = _libraryManager.GetItemById(playlistId) as Playlist; - - if (playlist == null) + if (!(_libraryManager.GetItemById(playlistId) is Playlist playlist)) { throw new ArgumentException("No Playlist exists with the supplied Id"); } @@ -254,7 +253,7 @@ namespace Emby.Server.Implementations.Playlists SavePlaylistFile(playlist); } - _providerManager.QueueRefresh(playlist.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + _providerManager.QueueRefresh(playlist.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true @@ -263,9 +262,7 @@ namespace Emby.Server.Implementations.Playlists public void MoveItem(string playlistId, string entryId, int newIndex) { - var playlist = _libraryManager.GetItemById(playlistId) as Playlist; - - if (playlist == null) + if (!(_libraryManager.GetItemById(playlistId) is Playlist playlist)) { throw new ArgumentException("No Playlist exists with the supplied Id"); } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index 2f07ff15a..ecd526251 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -19,16 +19,17 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.ScheduledTasks { /// - /// Class ChapterImagesTask + /// Class ChapterImagesTask. /// public class ChapterImagesTask : IScheduledTask { /// - /// The _logger + /// The _logger. /// private readonly ILogger _logger; + /// - /// The _library manager + /// The _library manager. /// private readonly ILibraryManager _libraryManager; @@ -53,12 +54,12 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// - /// Creates the triggers that define when the task will run + /// Creates the triggers that define when the task will run. /// public IEnumerable GetDefaultTriggers() { - return new[] { - + return new[] + { new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerDaily, @@ -117,7 +118,7 @@ namespace Emby.Server.Implementations.ScheduledTasks previouslyFailedImages = new List(); } - var directoryService = new DirectoryService(_logger, _fileSystem); + var directoryService = new DirectoryService(_fileSystem); foreach (var video in videos) { diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs index 2f5a8af80..46195cc74 100644 --- a/Emby.Server.Implementations/ServerApplicationPaths.cs +++ b/Emby.Server.Implementations/ServerApplicationPaths.cs @@ -6,7 +6,7 @@ using MediaBrowser.Controller; namespace Emby.Server.Implementations { /// - /// Extends BaseApplicationPaths to add paths that are only applicable on the server + /// Extends BaseApplicationPaths to add paths that are only applicable on the server. /// public class ServerApplicationPaths : BaseApplicationPaths, IServerApplicationPaths { diff --git a/MediaBrowser.Api/IHasItemFields.cs b/MediaBrowser.Api/IHasItemFields.cs index 8598ea262..85b4a7e2d 100644 --- a/MediaBrowser.Api/IHasItemFields.cs +++ b/MediaBrowser.Api/IHasItemFields.cs @@ -32,7 +32,7 @@ namespace MediaBrowser.Api if (string.IsNullOrEmpty(val)) { - return new ItemFields[] { }; + return Array.Empty(); } return val.Split(',').Select(v => @@ -41,6 +41,7 @@ namespace MediaBrowser.Api { return (ItemFields?)value; } + return null; }).Where(i => i.HasValue).Select(i => i.Value).ToArray(); diff --git a/MediaBrowser.Api/ItemLookupService.cs b/MediaBrowser.Api/ItemLookupService.cs index f3ea7907f..084b20bc1 100644 --- a/MediaBrowser.Api/ItemLookupService.cs +++ b/MediaBrowser.Api/ItemLookupService.cs @@ -227,15 +227,17 @@ namespace MediaBrowser.Api //item.ProductionYear = request.ProductionYear; //item.Name = request.Name; - return _providerManager.RefreshFullItem(item, new MetadataRefreshOptions(new DirectoryService(Logger, _fileSystem)) - { - MetadataRefreshMode = MetadataRefreshMode.FullRefresh, - ImageRefreshMode = MetadataRefreshMode.FullRefresh, - ReplaceAllMetadata = true, - ReplaceAllImages = request.ReplaceAllImages, - SearchResult = request - - }, CancellationToken.None); + return _providerManager.RefreshFullItem( + item, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + MetadataRefreshMode = MetadataRefreshMode.FullRefresh, + ImageRefreshMode = MetadataRefreshMode.FullRefresh, + ReplaceAllMetadata = true, + ReplaceAllImages = request.ReplaceAllImages, + SearchResult = request + }, + CancellationToken.None); } /// @@ -294,11 +296,9 @@ namespace MediaBrowser.Api Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); using (var stream = result.Content) + using (var filestream = _fileSystem.GetFileStream(fullCachePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) { - using (var filestream = _fileSystem.GetFileStream(fullCachePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) - { - await stream.CopyToAsync(filestream).ConfigureAwait(false); - } + await stream.CopyToAsync(filestream).ConfigureAwait(false); } Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); @@ -311,9 +311,6 @@ namespace MediaBrowser.Api /// The filename. /// System.String. private string GetFullCachePath(string filename) - { - return Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); - } - + => Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); } } diff --git a/MediaBrowser.Api/ItemRefreshService.cs b/MediaBrowser.Api/ItemRefreshService.cs index 8238ad19c..a1d69cd2b 100644 --- a/MediaBrowser.Api/ItemRefreshService.cs +++ b/MediaBrowser.Api/ItemRefreshService.cs @@ -63,7 +63,7 @@ namespace MediaBrowser.Api private MetadataRefreshOptions GetRefreshOptions(RefreshItem request) { - return new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + return new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { MetadataRefreshMode = request.MetadataRefreshMode, ImageRefreshMode = request.ImageRefreshMode, diff --git a/MediaBrowser.Api/ItemUpdateService.cs b/MediaBrowser.Api/ItemUpdateService.cs index d6514d62e..5d524b185 100644 --- a/MediaBrowser.Api/ItemUpdateService.cs +++ b/MediaBrowser.Api/ItemUpdateService.cs @@ -225,13 +225,15 @@ namespace MediaBrowser.Api if (displayOrderChanged) { - _providerManager.QueueRefresh(series.Id, new MetadataRefreshOptions(new DirectoryService(Logger, _fileSystem)) - { - MetadataRefreshMode = MetadataRefreshMode.FullRefresh, - ImageRefreshMode = MetadataRefreshMode.FullRefresh, - ReplaceAllMetadata = true - - }, RefreshPriority.High); + _providerManager.QueueRefresh( + series.Id, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + MetadataRefreshMode = MetadataRefreshMode.FullRefresh, + ImageRefreshMode = MetadataRefreshMode.FullRefresh, + ReplaceAllMetadata = true + }, + RefreshPriority.High); } } diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index 52043d3df..1878f51d0 100644 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ b/MediaBrowser.Api/Subtitles/SubtitleService.cs @@ -279,13 +279,12 @@ namespace MediaBrowser.Api.Subtitles await _subtitleManager.DownloadSubtitles(video, request.SubtitleId, CancellationToken.None) .ConfigureAwait(false); - _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(Logger, _fileSystem)), RefreshPriority.High); + _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); } catch (Exception ex) { Logger.LogError(ex, "Error downloading subtitles"); } - }); } } diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs index 45694a678..da0bf6dcb 100644 --- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs +++ b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs @@ -413,7 +413,7 @@ namespace MediaBrowser.Api.UserLibrary if (!hasMetdata) { - var options = new MetadataRefreshOptions(new DirectoryService(Logger, _fileSystem)) + var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { MetadataRefreshMode = MetadataRefreshMode.FullRefresh, ImageRefreshMode = MetadataRefreshMode.FullRefresh, diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index cacda8140..54540e892 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -89,7 +89,7 @@ namespace MediaBrowser.Controller.Entities { var locations = PhysicalLocations; - var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations; + var newLocations = CreateResolveArgs(new DirectoryService(FileSystem), false).PhysicalLocations; if (!locations.SequenceEqual(newLocations)) { diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 599d41bb2..1fd706857 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1344,7 +1344,7 @@ namespace MediaBrowser.Controller.Entities public Task RefreshMetadata(CancellationToken cancellationToken) { - return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)), cancellationToken); + return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken); } protected virtual void TriggerOnRefreshStart() @@ -2197,7 +2197,7 @@ namespace MediaBrowser.Controller.Entities /// Task. public virtual void ChangedExternally() { - ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)) + ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)) { }, RefreshPriority.High); diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index bc5e7467e..e5adf88d1 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -171,7 +171,7 @@ namespace MediaBrowser.Controller.Entities { var locations = PhysicalLocations; - var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations; + var newLocations = CreateResolveArgs(new DirectoryService(FileSystem), false).PhysicalLocations; if (!locations.SequenceEqual(newLocations)) { diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 61cc208d7..1cf7bb9b2 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -216,7 +216,7 @@ namespace MediaBrowser.Controller.Entities public Task ValidateChildren(IProgress progress, CancellationToken cancellationToken) { - return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem))); + return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(FileSystem))); } /// diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs index c70ecccf1..53601a610 100644 --- a/MediaBrowser.Controller/Entities/User.cs +++ b/MediaBrowser.Controller/Entities/User.cs @@ -148,7 +148,7 @@ namespace MediaBrowser.Controller.Entities Name = newName; return RefreshMetadata( - new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)) + new MetadataRefreshOptions(new DirectoryService(FileSystem)) { ReplaceAllMetadata = true, ImageRefreshMode = MetadataRefreshMode.FullRefresh, diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index 133e7c115..303b03a21 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -2,13 +2,11 @@ using System; using System.Collections.Generic; using System.Linq; using MediaBrowser.Model.IO; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Providers { public class DirectoryService : IDirectoryService { - private readonly ILogger _logger; private readonly IFileSystem _fileSystem; private readonly Dictionary _cache = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -17,9 +15,8 @@ namespace MediaBrowser.Controller.Providers private readonly Dictionary> _filePathCache = new Dictionary>(StringComparer.OrdinalIgnoreCase); - public DirectoryService(ILogger logger, IFileSystem fileSystem) + public DirectoryService(IFileSystem fileSystem) { - _logger = logger; _fileSystem = fileSystem; } @@ -27,8 +24,6 @@ namespace MediaBrowser.Controller.Providers { if (!_cache.TryGetValue(path, out FileSystemMetadata[] entries)) { - //_logger.LogDebug("Getting files for " + path); - entries = _fileSystem.GetFileSystemEntries(path).ToArray(); //_cache.TryAdd(path, entries); @@ -49,6 +44,7 @@ namespace MediaBrowser.Controller.Providers list.Add(item); } } + return list; } @@ -89,6 +85,5 @@ namespace MediaBrowser.Controller.Providers return result; } - } } diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs index 0872335c5..39538aacd 100644 --- a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs +++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs @@ -24,7 +24,8 @@ namespace MediaBrowser.Controller.Subtitles /// /// Searches the subtitles. /// - Task SearchSubtitles(Video video, + Task SearchSubtitles( + Video video, string language, bool? isPerfectMatch, CancellationToken cancellationToken); @@ -34,8 +35,9 @@ namespace MediaBrowser.Controller.Subtitles /// /// The request. /// The cancellation token. - /// Task{IEnumerable{RemoteSubtitleInfo}}. - Task SearchSubtitles(SubtitleSearchRequest request, + /// Task{RemoteSubtitleInfo[]}. + Task SearchSubtitles( + SubtitleSearchRequest request, CancellationToken cancellationToken); /// @@ -53,7 +55,7 @@ namespace MediaBrowser.Controller.Subtitles /// /// The identifier. /// The cancellation token. - /// Task{SubtitleResponse}. + /// . Task GetRemoteSubtitles(string id, CancellationToken cancellationToken); /// diff --git a/MediaBrowser.Model/IO/StreamDefaults.cs b/MediaBrowser.Model/IO/StreamDefaults.cs index 1dc29e06e..4b55ce1f3 100644 --- a/MediaBrowser.Model/IO/StreamDefaults.cs +++ b/MediaBrowser.Model/IO/StreamDefaults.cs @@ -1,17 +1,17 @@ namespace MediaBrowser.Model.IO { /// - /// Class StreamDefaults + /// Class StreamDefaults. /// public static class StreamDefaults { /// - /// The default copy to buffer size + /// The default copy to buffer size. /// public const int DefaultCopyToBufferSize = 81920; /// - /// The default file stream buffer size + /// The default file stream buffer size. /// public const int DefaultFileStreamBufferSize = 4096; } diff --git a/MediaBrowser.Providers/Books/AudioBookMetadataService.cs b/MediaBrowser.Providers/Books/AudioBookMetadataService.cs index 0062d5ab3..309241bfa 100644 --- a/MediaBrowser.Providers/Books/AudioBookMetadataService.cs +++ b/MediaBrowser.Providers/Books/AudioBookMetadataService.cs @@ -16,9 +16,8 @@ namespace MediaBrowser.Providers.Books ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, - IUserDataManager userDataManager, ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { } diff --git a/MediaBrowser.Providers/Books/BookMetadataService.cs b/MediaBrowser.Providers/Books/BookMetadataService.cs index 358f87a0f..9d6a1ef59 100644 --- a/MediaBrowser.Providers/Books/BookMetadataService.cs +++ b/MediaBrowser.Providers/Books/BookMetadataService.cs @@ -11,6 +11,17 @@ namespace MediaBrowser.Providers.Books { public class BookMetadataService : MetadataService { + public BookMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) + { + } + + /// protected override void MergeData(MetadataResult source, MetadataResult target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); @@ -20,9 +31,5 @@ namespace MediaBrowser.Providers.Books target.Item.SeriesName = source.Item.SeriesName; } } - - public BookMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) - { - } } } diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs index 693edb143..5bf01232c 100644 --- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs +++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs @@ -14,11 +14,35 @@ namespace MediaBrowser.Providers.BoxSets { public class BoxSetMetadataService : MetadataService { + public BoxSetMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) + { + } + + /// + protected override bool EnableUpdatingGenresFromChildren => true; + + /// + protected override bool EnableUpdatingOfficialRatingFromChildren => true; + + /// + protected override bool EnableUpdatingStudiosFromChildren => true; + + /// + protected override bool EnableUpdatingPremiereDateFromChildren => true; + + /// protected override IList GetChildrenForMetadataUpdates(BoxSet item) { return item.GetLinkedChildren(); } + /// protected override void MergeData(MetadataResult source, MetadataResult target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); @@ -32,6 +56,7 @@ namespace MediaBrowser.Providers.BoxSets } } + /// protected override ItemUpdateType BeforeSaveInternal(BoxSet item, bool isFullRefresh, ItemUpdateType currentUpdateType) { var updateType = base.BeforeSaveInternal(item, isFullRefresh, currentUpdateType); @@ -47,17 +72,5 @@ namespace MediaBrowser.Providers.BoxSets return updateType; } - - public BoxSetMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) - { - } - - protected override bool EnableUpdatingGenresFromChildren => true; - - protected override bool EnableUpdatingOfficialRatingFromChildren => true; - - protected override bool EnableUpdatingStudiosFromChildren => true; - - protected override bool EnableUpdatingPremiereDateFromChildren => true; } } diff --git a/MediaBrowser.Providers/Channels/ChannelMetadataService.cs b/MediaBrowser.Providers/Channels/ChannelMetadataService.cs index 0763a0163..da41f208c 100644 --- a/MediaBrowser.Providers/Channels/ChannelMetadataService.cs +++ b/MediaBrowser.Providers/Channels/ChannelMetadataService.cs @@ -11,13 +11,20 @@ namespace MediaBrowser.Providers.Channels { public class ChannelMetadataService : MetadataService { - protected override void MergeData(MetadataResult source, MetadataResult target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + public ChannelMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - public ChannelMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + /// + protected override void MergeData(MetadataResult source, MetadataResult target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } } } diff --git a/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs b/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs index 2c28b3e35..dd1b4709d 100644 --- a/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs +++ b/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs @@ -12,13 +12,20 @@ namespace MediaBrowser.Providers.Folders { public class CollectionFolderMetadataService : MetadataService { - protected override void MergeData(MetadataResult source, MetadataResult target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + public CollectionFolderMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - public CollectionFolderMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + /// + protected override void MergeData(MetadataResult source, MetadataResult target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } } } diff --git a/MediaBrowser.Providers/Folders/FolderMetadataService.cs b/MediaBrowser.Providers/Folders/FolderMetadataService.cs index bb1634422..8409e03fd 100644 --- a/MediaBrowser.Providers/Folders/FolderMetadataService.cs +++ b/MediaBrowser.Providers/Folders/FolderMetadataService.cs @@ -11,16 +11,24 @@ namespace MediaBrowser.Providers.Folders { public class FolderMetadataService : MetadataService { + public FolderMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) + { + } + + /// // Make sure the type-specific services get picked first public override int Order => 10; + /// protected override void MergeData(MetadataResult source, MetadataResult target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - - public FolderMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) - { - } } } diff --git a/MediaBrowser.Providers/Folders/UserViewMetadataService.cs b/MediaBrowser.Providers/Folders/UserViewMetadataService.cs index 8d6e77aeb..2ceb71afc 100644 --- a/MediaBrowser.Providers/Folders/UserViewMetadataService.cs +++ b/MediaBrowser.Providers/Folders/UserViewMetadataService.cs @@ -11,13 +11,20 @@ namespace MediaBrowser.Providers.Folders { public class UserViewMetadataService : MetadataService { - protected override void MergeData(MetadataResult source, MetadataResult target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + public UserViewMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - public UserViewMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + /// + protected override void MergeData(MetadataResult source, MetadataResult target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } } } diff --git a/MediaBrowser.Providers/Genres/GenreMetadataService.cs b/MediaBrowser.Providers/Genres/GenreMetadataService.cs index 28a46b008..932eb368c 100644 --- a/MediaBrowser.Providers/Genres/GenreMetadataService.cs +++ b/MediaBrowser.Providers/Genres/GenreMetadataService.cs @@ -11,13 +11,20 @@ namespace MediaBrowser.Providers.Genres { public class GenreMetadataService : MetadataService { - protected override void MergeData(MetadataResult source, MetadataResult target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + public GenreMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - public GenreMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + /// + protected override void MergeData(MetadataResult source, MetadataResult target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } } } diff --git a/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs b/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs index 6009d3a58..13dd97215 100644 --- a/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs +++ b/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs @@ -11,13 +11,20 @@ namespace MediaBrowser.Providers.LiveTv { public class LiveTvMetadataService : MetadataService { - protected override void MergeData(MetadataResult source, MetadataResult target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + public LiveTvMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - public LiveTvMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + /// + protected override void MergeData(MetadataResult source, MetadataResult target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } } } diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 033aea146..e9179815e 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -340,7 +340,7 @@ namespace MediaBrowser.Providers.Manager if (deleted) { - item.ValidateImages(new DirectoryService(_logger, _fileSystem)); + item.ValidateImages(new DirectoryService(_fileSystem)); } } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index f0716f201..c3401f12b 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -23,16 +23,14 @@ namespace MediaBrowser.Providers.Manager protected readonly ILogger Logger; protected readonly IProviderManager ProviderManager; protected readonly IFileSystem FileSystem; - protected readonly IUserDataManager UserDataManager; protected readonly ILibraryManager LibraryManager; - protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) + protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) { ServerConfigurationManager = serverConfigurationManager; Logger = logger; ProviderManager = providerManager; FileSystem = fileSystem; - UserDataManager = userDataManager; LibraryManager = libraryManager; } @@ -44,7 +42,7 @@ namespace MediaBrowser.Providers.Manager } catch (Exception ex) { - Logger.LogError(ex, "Error getting file {path}", path); + Logger.LogError(ex, "Error getting file {Path}", path); return null; } } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 6a8d03f6d..631d063a5 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -328,7 +328,7 @@ namespace MediaBrowser.Providers.Manager return GetImageProviders(item, libraryOptions, options, new ImageRefreshOptions( - new DirectoryService(_logger, _fileSystem)), + new DirectoryService(_fileSystem)), includeDisabled) .OfType(); } @@ -507,7 +507,7 @@ namespace MediaBrowser.Providers.Manager var imageProviders = GetImageProviders(dummy, libraryOptions, options, new ImageRefreshOptions( - new DirectoryService(_logger, _fileSystem)), + new DirectoryService(_fileSystem)), true) .ToList(); diff --git a/MediaBrowser.Providers/Movies/MovieExternalIds.cs b/MediaBrowser.Providers/Movies/MovieExternalIds.cs index 09ed6034c..55810b1ed 100644 --- a/MediaBrowser.Providers/Movies/MovieExternalIds.cs +++ b/MediaBrowser.Providers/Movies/MovieExternalIds.cs @@ -9,17 +9,20 @@ namespace MediaBrowser.Providers.Movies { public class ImdbExternalId : IExternalId { + /// public string Name => "IMDb"; + /// public string Key => MetadataProviders.Imdb.ToString(); + /// public string UrlFormatString => "https://www.imdb.com/title/{0}"; + /// public bool Supports(IHasProviderIds item) { // Supports images for tv movies - var tvProgram = item as LiveTvProgram; - if (tvProgram != null && tvProgram.IsMovie) + if (item is LiveTvProgram tvProgram && tvProgram.IsMovie) { return true; } @@ -28,18 +31,18 @@ namespace MediaBrowser.Providers.Movies } } - public class ImdbPersonExternalId : IExternalId { + /// public string Name => "IMDb"; + /// public string Key => MetadataProviders.Imdb.ToString(); + /// public string UrlFormatString => "https://www.imdb.com/name/{0}"; - public bool Supports(IHasProviderIds item) - { - return item is Person; - } + /// + public bool Supports(IHasProviderIds item) => item is Person; } } diff --git a/MediaBrowser.Providers/Movies/MovieMetadataService.cs b/MediaBrowser.Providers/Movies/MovieMetadataService.cs index 41806e49b..c6cc5c7dc 100644 --- a/MediaBrowser.Providers/Movies/MovieMetadataService.cs +++ b/MediaBrowser.Providers/Movies/MovieMetadataService.cs @@ -1,5 +1,4 @@ using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -12,6 +11,17 @@ namespace MediaBrowser.Providers.Movies { public class MovieMetadataService : MetadataService { + public MovieMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) + { + } + + /// protected override bool IsFullLocalMetadata(Movie item) { if (string.IsNullOrWhiteSpace(item.Overview)) @@ -25,6 +35,7 @@ namespace MediaBrowser.Providers.Movies return base.IsFullLocalMetadata(item); } + /// protected override void MergeData(MetadataResult source, MetadataResult target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); @@ -37,40 +48,5 @@ namespace MediaBrowser.Providers.Movies targetItem.CollectionName = sourceItem.CollectionName; } } - - public MovieMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) - { - } } - - public class TrailerMetadataService : MetadataService - { - protected override bool IsFullLocalMetadata(Trailer item) - { - if (string.IsNullOrWhiteSpace(item.Overview)) - { - return false; - } - if (!item.ProductionYear.HasValue) - { - return false; - } - return base.IsFullLocalMetadata(item); - } - - protected override void MergeData(MetadataResult source, MetadataResult target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) - { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); - - if (replaceData || target.Item.TrailerTypes.Length == 0) - { - target.Item.TrailerTypes = source.Item.TrailerTypes; - } - } - - public TrailerMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) - { - } - } - } diff --git a/MediaBrowser.Providers/Movies/TrailerMetadataService.cs b/MediaBrowser.Providers/Movies/TrailerMetadataService.cs new file mode 100644 index 000000000..53b556940 --- /dev/null +++ b/MediaBrowser.Providers/Movies/TrailerMetadataService.cs @@ -0,0 +1,49 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Providers.Manager; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Providers.Movies +{ + public class TrailerMetadataService : MetadataService + { + public TrailerMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) + { + } + + /// + protected override bool IsFullLocalMetadata(Trailer item) + { + if (string.IsNullOrWhiteSpace(item.Overview)) + { + return false; + } + if (!item.ProductionYear.HasValue) + { + return false; + } + return base.IsFullLocalMetadata(item); + } + + /// + protected override void MergeData(MetadataResult source, MetadataResult target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); + + if (replaceData || target.Item.TrailerTypes.Length == 0) + { + target.Item.TrailerTypes = source.Item.TrailerTypes; + } + } + } +} diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs index 4e59b4119..69133c1c1 100644 --- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs +++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs @@ -20,9 +20,8 @@ namespace MediaBrowser.Providers.Music ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, - IUserDataManager userDataManager, ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { } @@ -37,10 +36,7 @@ namespace MediaBrowser.Providers.Music /// protected override IList GetChildrenForMetadataUpdates(MusicAlbum item) - { - return item.GetRecursiveChildren(i => i is Audio) - .ToList(); - } + => item.GetRecursiveChildren(i => i is Audio); /// protected override ItemUpdateType UpdateMetadataFromChildren(MusicAlbum item, IList children, bool isFullRefresh, ItemUpdateType currentUpdateType) @@ -53,20 +49,18 @@ namespace MediaBrowser.Providers.Music { var name = children.Select(i => i.Album).FirstOrDefault(i => !string.IsNullOrEmpty(i)); - if (!string.IsNullOrEmpty(name)) + if (!string.IsNullOrEmpty(name) + && !string.Equals(item.Name, name, StringComparison.Ordinal)) { - if (!string.Equals(item.Name, name, StringComparison.Ordinal)) - { - item.Name = name; - updateType = updateType | ItemUpdateType.MetadataEdit; - } + item.Name = name; + updateType |= ItemUpdateType.MetadataEdit; } } var songs = children.Cast