diff options
Diffstat (limited to 'MediaBrowser.Common')
49 files changed, 3397 insertions, 761 deletions
diff --git a/MediaBrowser.Common/Configuration/ConfigurationStore.cs b/MediaBrowser.Common/Configuration/ConfigurationStore.cs new file mode 100644 index 000000000..050ab1ab5 --- /dev/null +++ b/MediaBrowser.Common/Configuration/ConfigurationStore.cs @@ -0,0 +1,22 @@ +#nullable disable + +using System; + +namespace MediaBrowser.Common.Configuration +{ + /// <summary> + /// Describes a single entry in the application configuration. + /// </summary> + public class ConfigurationStore + { + /// <summary> + /// Gets or sets the unique identifier for the configuration. + /// </summary> + public string Key { get; set; } + + /// <summary> + /// Gets or sets the type used to store the data for this configuration entry. + /// </summary> + public Type ConfigurationType { get; set; } + } +} diff --git a/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs b/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs index 0b59627cc..2df87d879 100644 --- a/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs +++ b/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs @@ -1,3 +1,6 @@ +#nullable disable +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Common.Configuration @@ -9,6 +12,7 @@ namespace MediaBrowser.Common.Configuration /// </summary> /// <value>The key.</value> public string Key { get; set; } + /// <summary> /// Gets or sets the new configuration. /// </summary> diff --git a/MediaBrowser.Common/Configuration/EncodingConfigurationExtensions.cs b/MediaBrowser.Common/Configuration/EncodingConfigurationExtensions.cs new file mode 100644 index 000000000..89740ae08 --- /dev/null +++ b/MediaBrowser.Common/Configuration/EncodingConfigurationExtensions.cs @@ -0,0 +1,43 @@ +using System; +using System.IO; +using MediaBrowser.Model.Configuration; + +namespace MediaBrowser.Common.Configuration +{ + /// <summary> + /// Class containing extension methods for working with the encoding configuration. + /// </summary> + public static class EncodingConfigurationExtensions + { + /// <summary> + /// Gets the encoding options. + /// </summary> + /// <param name="configurationManager">The configuration manager.</param> + /// <returns>The encoding options.</returns> + public static EncodingOptions GetEncodingOptions(this IConfigurationManager configurationManager) + => configurationManager.GetConfiguration<EncodingOptions>("encoding"); + + /// <summary> + /// Retrieves the transcoding temp path from the encoding configuration, falling back to a default if no path + /// is specified in configuration. If the directory does not exist, it will be created. + /// </summary> + /// <param name="configurationManager">The configuration manager.</param> + /// <returns>The transcoding temp path.</returns> + /// <exception cref="UnauthorizedAccessException">If the directory does not exist, and the caller does not have the required permission to create it.</exception> + /// <exception cref="NotSupportedException">If there is a custom path transcoding path specified, but it is invalid.</exception> + /// <exception cref="IOException">If the directory does not exist, and it also could not be created.</exception> + public static string GetTranscodePath(this IConfigurationManager configurationManager) + { + // Get the configured path and fall back to a default + var transcodingTempPath = configurationManager.GetEncodingOptions().TranscodingTempPath; + if (string.IsNullOrEmpty(transcodingTempPath)) + { + transcodingTempPath = Path.Combine(configurationManager.CommonApplicationPaths.ProgramDataPath, "transcodes"); + } + + // Make sure the directory exists + Directory.CreateDirectory(transcodingTempPath); + return transcodingTempPath; + } + } +} diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs index cb4e8bf5f..1370e6d79 100644 --- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs +++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs @@ -1,24 +1,34 @@ +#nullable disable + namespace MediaBrowser.Common.Configuration { /// <summary> - /// Interface IApplicationPaths + /// Interface IApplicationPaths. /// </summary> public interface IApplicationPaths { /// <summary> - /// Gets the path to the program data folder + /// Gets the path to the program data folder. /// </summary> /// <value>The program data path.</value> string ProgramDataPath { get; } /// <summary> - /// Gets the path to the program system folder + /// Gets the path to the web UI resources folder. + /// </summary> + /// <remarks> + /// This value is not relevant if the server is configured to not host any static web content. + /// </remarks> + string WebPath { get; } + + /// <summary> + /// Gets the path to the program system folder. /// </summary> /// <value>The program data path.</value> string ProgramSystemPath { get; } /// <summary> - /// Gets the folder path to the data directory + /// Gets the folder path to the data directory. /// </summary> /// <value>The data directory.</value> string DataPath { get; } @@ -30,48 +40,51 @@ namespace MediaBrowser.Common.Configuration string ImageCachePath { get; } /// <summary> - /// Gets the path to the plugin directory + /// Gets the path to the plugin directory. /// </summary> /// <value>The plugins path.</value> string PluginsPath { get; } /// <summary> - /// Gets the path to the plugin configurations directory + /// Gets the path to the plugin configurations directory. /// </summary> /// <value>The plugin configurations path.</value> string PluginConfigurationsPath { get; } /// <summary> - /// Gets the path to the log directory + /// Gets the path to the log directory. /// </summary> /// <value>The log directory path.</value> string LogDirectoryPath { get; } /// <summary> - /// Gets the path to the application configuration root directory + /// Gets the path to the application configuration root directory. /// </summary> /// <value>The configuration directory path.</value> string ConfigurationDirectoryPath { get; } /// <summary> - /// Gets the path to the system configuration file + /// Gets the path to the system configuration file. /// </summary> /// <value>The system configuration file path.</value> string SystemConfigurationFilePath { get; } /// <summary> - /// Gets the folder path to the cache directory + /// Gets the folder path to the cache directory. /// </summary> /// <value>The cache directory.</value> string CachePath { get; } /// <summary> - /// Gets the folder path to the temp directory within the cache folder + /// Gets the folder path to the temp directory within the cache folder. /// </summary> /// <value>The temp directory.</value> string TempDirectory { get; } + /// <summary> + /// Gets the magic string used for virtual path manipulation. + /// </summary> + /// <value>The magic string used for virtual path manipulation.</value> string VirtualDataPath { get; } } - } diff --git a/MediaBrowser.Common/Configuration/IConfigurationFactory.cs b/MediaBrowser.Common/Configuration/IConfigurationFactory.cs index 0fb2b83d1..6db1f1364 100644 --- a/MediaBrowser.Common/Configuration/IConfigurationFactory.cs +++ b/MediaBrowser.Common/Configuration/IConfigurationFactory.cs @@ -1,22 +1,17 @@ -using System; using System.Collections.Generic; namespace MediaBrowser.Common.Configuration { + /// <summary> + /// Provides an interface to retrieve a configuration store. Classes with this interface are scanned for at + /// application start to dynamically register configuration for various modules/plugins. + /// </summary> public interface IConfigurationFactory { + /// <summary> + /// Get the configuration store for this module. + /// </summary> + /// <returns>The configuration store.</returns> IEnumerable<ConfigurationStore> GetConfigurations(); } - - public class ConfigurationStore - { - public string Key { get; set; } - - public Type ConfigurationType { get; set; } - } - - public interface IValidatingConfiguration - { - void Validate(object oldConfig, object newConfig); - } } diff --git a/MediaBrowser.Common/Configuration/IConfigurationManager.cs b/MediaBrowser.Common/Configuration/IConfigurationManager.cs index 8fed2dcdf..fc63d9350 100644 --- a/MediaBrowser.Common/Configuration/IConfigurationManager.cs +++ b/MediaBrowser.Common/Configuration/IConfigurationManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.Configuration; @@ -22,7 +24,7 @@ namespace MediaBrowser.Common.Configuration event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdated; /// <summary> - /// Gets or sets the application paths. + /// Gets the application paths. /// </summary> /// <value>The application paths.</value> IApplicationPaths CommonApplicationPaths { get; } @@ -45,6 +47,13 @@ namespace MediaBrowser.Common.Configuration void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration); /// <summary> + /// Manually pre-loads a factory so that it is available pre system initialisation. + /// </summary> + /// <typeparam name="T">Class to register.</typeparam> + void RegisterConfiguration<T>() + where T : IConfigurationFactory; + + /// <summary> /// Gets the configuration. /// </summary> /// <param name="key">The key.</param> diff --git a/MediaBrowser.Common/Configuration/IValidatingConfiguration.cs b/MediaBrowser.Common/Configuration/IValidatingConfiguration.cs new file mode 100644 index 000000000..3b1d84f3c --- /dev/null +++ b/MediaBrowser.Common/Configuration/IValidatingConfiguration.cs @@ -0,0 +1,15 @@ +namespace MediaBrowser.Common.Configuration +{ + /// <summary> + /// A configuration store that can be validated. + /// </summary> + public interface IValidatingConfiguration + { + /// <summary> + /// Validation method to be invoked before saving the configuration. + /// </summary> + /// <param name="oldConfig">The old configuration.</param> + /// <param name="newConfig">The new configuration.</param> + void Validate(object oldConfig, object newConfig); + } +} diff --git a/MediaBrowser.Common/Crc32.cs b/MediaBrowser.Common/Crc32.cs new file mode 100644 index 000000000..599eb4c99 --- /dev/null +++ b/MediaBrowser.Common/Crc32.cs @@ -0,0 +1,89 @@ +#pragma warning disable CS1591 + +using System; + +namespace MediaBrowser.Common +{ + public static class Crc32 + { + private static readonly uint[] _crcTable = + { + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, + 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, + 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, + 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, + 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, + 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, + 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, + 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, + 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, + 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, + 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, + 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, + 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, + 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, + 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, + 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, + 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, + 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, + 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, + 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, + 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, + 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, + 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, + 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, + 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, + 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, + 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, + 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, + 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, + 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, + 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, + 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, + 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, + 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, + 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, + 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, + 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, + 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, + 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, + 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, + 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, + 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, + 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, + 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, + 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, + 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, + 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, + 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, + 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, + 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, + 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, + 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, + 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, + 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, + 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, + 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, + 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, + 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, + 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, + 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, + 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, + 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d + }; + + public static uint Compute(ReadOnlySpan<byte> bytes) + { + var crc = 0xffffffff; + var len = bytes.Length; + for (var i = 0; i < len; i++) + { + crc = (crc >> 8) ^ _crcTable[(bytes[i] ^ crc) & 0xff]; + } + + return ~crc; + } + } +} 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 +{ + /// <summary> + /// Class containing global constants for Jellyfin Cryptography. + /// </summary> + public static class Constants + { + /// <summary> + /// The default length for new salts. + /// </summary> + public const int DefaultSaltLength = 64; + + /// <summary> + /// The default amount of iterations for hashing passwords. + /// </summary> + public const int DefaultIterations = 1000; + } +} diff --git a/MediaBrowser.Common/Cryptography/CryptoExtensions.cs b/MediaBrowser.Common/Cryptography/CryptoExtensions.cs new file mode 100644 index 000000000..157b0ed10 --- /dev/null +++ b/MediaBrowser.Common/Cryptography/CryptoExtensions.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 +{ + /// <summary> + /// Class containing extension methods for working with Jellyfin cryptography objects. + /// </summary> + public static class CryptoExtensions + { + /// <summary> + /// Creates a new <see cref="PasswordHash" /> instance. + /// </summary> + /// <param name="cryptoProvider">The <see cref="ICryptoProvider" /> instance used.</param> + /// <param name="password">The password that will be hashed.</param> + /// <returns>A <see cref="PasswordHash" /> instance with the hash method, hash, salt and number of iterations.</returns> + public static PasswordHash CreatePasswordHash(this ICryptoProvider cryptoProvider, string password) + { + byte[] salt = cryptoProvider.GenerateSalt(); + return new PasswordHash( + cryptoProvider.DefaultHashMethod, + cryptoProvider.ComputeHashWithDefaultMethod( + Encoding.UTF8.GetBytes(password), + salt), + salt, + new Dictionary<string, string> + { + { "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) } + }); + } + } +} diff --git a/MediaBrowser.Common/Cryptography/PasswordHash.cs b/MediaBrowser.Common/Cryptography/PasswordHash.cs new file mode 100644 index 000000000..0e2065302 --- /dev/null +++ b/MediaBrowser.Common/Cryptography/PasswordHash.cs @@ -0,0 +1,219 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Text; + +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 + // $<id>[$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]] + // 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<string, string> _parameters; + private readonly byte[] _salt; + private readonly byte[] _hash; + + public PasswordHash(string id, byte[] hash) + : this(id, hash, Array.Empty<byte>()) + { + } + + public PasswordHash(string id, byte[] hash, byte[] salt) + : this(id, hash, salt, new Dictionary<string, string>()) + { + } + + public PasswordHash(string id, byte[] hash, byte[] salt, Dictionary<string, string> parameters) + { + if (id == null) + { + throw new ArgumentNullException(nameof(id)); + } + + if (id.Length == 0) + { + throw new ArgumentException("String can't be empty", nameof(id)); + } + + Id = id; + _hash = hash; + _salt = salt; + _parameters = parameters; + } + + /// <summary> + /// Gets the symbolic name for the function used. + /// </summary> + /// <value>Returns the symbolic name for the function used.</value> + public string Id { get; } + + /// <summary> + /// Gets the additional parameters used by the hash function. + /// </summary> + public IReadOnlyDictionary<string, string> Parameters => _parameters; + + /// <summary> + /// Gets the salt used for hashing the password. + /// </summary> + /// <value>Returns the salt used for hashing the password.</value> + public ReadOnlySpan<byte> Salt => _salt; + + /// <summary> + /// Gets the hashed password. + /// </summary> + /// <value>Return the hashed password.</value> + public ReadOnlySpan<byte> Hash => _hash; + + public static PasswordHash Parse(ReadOnlySpan<char> hashString) + { + if (hashString.IsEmpty) + { + throw new ArgumentException("String can't be empty", nameof(hashString)); + } + + if (hashString[0] != '$') + { + throw new FormatException("Hash string must start with a $"); + } + + // Ignore first $ + hashString = hashString[1..]; + + int nextSegment = hashString.IndexOf('$'); + if (hashString.IsEmpty || nextSegment == 0) + { + throw new FormatException("Hash string must contain a valid id"); + } + else if (nextSegment == -1) + { + return new PasswordHash(hashString.ToString(), Array.Empty<byte>()); + } + + ReadOnlySpan<char> id = hashString[..nextSegment]; + hashString = hashString[(nextSegment + 1)..]; + Dictionary<string, string>? parameters = null; + + nextSegment = hashString.IndexOf('$'); + + // Optional parameters + ReadOnlySpan<char> parametersSpan = nextSegment == -1 ? hashString : hashString[..nextSegment]; + if (parametersSpan.Contains('=')) + { + while (!parametersSpan.IsEmpty) + { + ReadOnlySpan<char> parameter; + int index = parametersSpan.IndexOf(','); + if (index == -1) + { + parameter = parametersSpan; + parametersSpan = ReadOnlySpan<char>.Empty; + } + else + { + parameter = parametersSpan[..index]; + parametersSpan = parametersSpan[(index + 1)..]; + } + + int splitIndex = parameter.IndexOf('='); + if (splitIndex == -1 || splitIndex == 0 || splitIndex == parameter.Length - 1) + { + throw new FormatException("Malformed parameter in password hash string"); + } + + (parameters ??= new Dictionary<string, string>()).Add( + parameter[..splitIndex].ToString(), + parameter[(splitIndex + 1)..].ToString()); + } + + if (nextSegment == -1) + { + // parameters can't be null here + return new PasswordHash(id.ToString(), Array.Empty<byte>(), Array.Empty<byte>(), parameters!); + } + + hashString = hashString[(nextSegment + 1)..]; + nextSegment = hashString.IndexOf('$'); + } + + if (nextSegment == 0) + { + throw new FormatException("Hash string contains an empty segment"); + } + + byte[] hash; + byte[] salt; + + if (nextSegment == -1) + { + salt = Array.Empty<byte>(); + hash = Convert.FromHexString(hashString); + } + else + { + salt = Convert.FromHexString(hashString[..nextSegment]); + hashString = hashString[(nextSegment + 1)..]; + nextSegment = hashString.IndexOf('$'); + if (nextSegment != -1) + { + throw new FormatException("Hash string contains too many segments"); + } + + if (hashString.IsEmpty) + { + throw new FormatException("Hash segment is empty"); + } + + hash = Convert.FromHexString(hashString); + } + + return new PasswordHash(id.ToString(), hash, salt, parameters ?? new Dictionary<string, string>()); + } + + private void SerializeParameters(StringBuilder stringBuilder) + { + if (_parameters.Count == 0) + { + return; + } + + stringBuilder.Append('$'); + foreach (var pair in _parameters) + { + stringBuilder.Append(pair.Key) + .Append('=') + .Append(pair.Value) + .Append(','); + } + + // Remove last ',' + stringBuilder.Length -= 1; + } + + /// <inheritdoc /> + public override string ToString() + { + var str = new StringBuilder() + .Append('$') + .Append(Id); + SerializeParameters(str); + + if (_salt.Length != 0) + { + str.Append('$') + .Append(Convert.ToHexString(_salt)); + } + + if (_hash.Length != 0) + { + str.Append('$') + .Append(Convert.ToHexString(_hash)); + } + + return str.ToString(); + } + } +} diff --git a/MediaBrowser.Common/Events/EventHelper.cs b/MediaBrowser.Common/Events/EventHelper.cs index 0ac7905a1..a9cf86fbc 100644 --- a/MediaBrowser.Common/Events/EventHelper.cs +++ b/MediaBrowser.Common/Events/EventHelper.cs @@ -4,10 +4,10 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Common.Events { - // TODO: @bond Remove /// <summary> - /// Class EventHelper + /// Class EventHelper. /// </summary> + // TODO: @bond Remove public static class EventHelper { /// <summary> @@ -17,7 +17,7 @@ namespace MediaBrowser.Common.Events /// <param name="sender">The sender.</param> /// <param name="args">The <see cref="EventArgs" /> instance containing the event data.</param> /// <param name="logger">The logger.</param> - public static void QueueEventIfNotNull(EventHandler handler, object sender, EventArgs args, ILogger logger) + public static void QueueEventIfNotNull(EventHandler? handler, object sender, EventArgs args, ILogger logger) { if (handler != null) { @@ -38,12 +38,12 @@ namespace MediaBrowser.Common.Events /// <summary> /// Queues the event. /// </summary> - /// <typeparam name="T"></typeparam> + /// <typeparam name="T">Argument type for the <c>handler</c>.</typeparam> /// <param name="handler">The handler.</param> /// <param name="sender">The sender.</param> /// <param name="args">The args.</param> /// <param name="logger">The logger.</param> - public static void QueueEventIfNotNull<T>(EventHandler<T> handler, object sender, T args, ILogger logger) + public static void QueueEventIfNotNull<T>(EventHandler<T>? handler, object sender, T args, ILogger logger) { if (handler != null) { diff --git a/MediaBrowser.Common/Extensions/BaseExtensions.cs b/MediaBrowser.Common/Extensions/BaseExtensions.cs index db0514bb1..08964420e 100644 --- a/MediaBrowser.Common/Extensions/BaseExtensions.cs +++ b/MediaBrowser.Common/Extensions/BaseExtensions.cs @@ -1,37 +1,42 @@ using System; +using System.Security.Cryptography; +using System.Text; using System.Text.RegularExpressions; -using MediaBrowser.Model.Cryptography; namespace MediaBrowser.Common.Extensions { /// <summary> - /// Class BaseExtensions + /// Class BaseExtensions. /// </summary> public static class BaseExtensions { - public static ICryptoProvider CryptographyProvider { get; set; } - /// <summary> /// Strips the HTML. /// </summary> /// <param name="htmlString">The HTML string.</param> - /// <returns>System.String.</returns> + /// <returns><see cref="string" />.</returns> 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(); } /// <summary> - /// Gets the M d5. + /// Gets the Md5. /// </summary> - /// <param name="str">The STR.</param> - /// <returns>Guid.</returns> + /// <param name="str">The string.</param> + /// <returns><see cref="Guid" />.</returns> public static Guid GetMD5(this string str) { - return CryptographyProvider.GetMD5(str); +#pragma warning disable CA5351 + using (var provider = MD5.Create()) + { + return new Guid(provider.ComputeHash(Encoding.Unicode.GetBytes(str))); + } + +#pragma warning restore CA5351 } } } diff --git a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs new file mode 100644 index 000000000..1e5877c84 --- /dev/null +++ b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs @@ -0,0 +1,41 @@ +using System.Net; +using Microsoft.AspNetCore.Http; + +namespace MediaBrowser.Common.Extensions +{ + /// <summary> + /// Static class containing extension methods for <see cref="HttpContext"/>. + /// </summary> + public static class HttpContextExtensions + { + /// <summary> + /// Checks the origin of the HTTP context. + /// </summary> + /// <param name="context">The incoming HTTP context.</param> + /// <returns><c>true</c> if the request is coming from LAN, <c>false</c> otherwise.</returns> + public static bool IsLocal(this HttpContext context) + { + return (context.Connection.LocalIpAddress == null + && context.Connection.RemoteIpAddress == null) + || Equals(context.Connection.LocalIpAddress, context.Connection.RemoteIpAddress); + } + + /// <summary> + /// Extracts the remote IP address of the caller of the HTTP context. + /// </summary> + /// <param name="context">The HTTP context.</param> + /// <returns>The remote caller IP address.</returns> + public static IPAddress GetNormalizedRemoteIp(this HttpContext context) + { + // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests) + var ip = context.Connection.RemoteIpAddress ?? IPAddress.Loopback; + + if (ip.IsIPv4MappedToIPv6) + { + ip = ip.MapToIPv4(); + } + + return ip; + } + } +} diff --git a/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs b/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs new file mode 100644 index 000000000..48e758ee4 --- /dev/null +++ b/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs @@ -0,0 +1,26 @@ +using System; + +namespace MediaBrowser.Common.Extensions +{ + /// <summary> + /// Class MethodNotAllowedException. + /// </summary> + public class MethodNotAllowedException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="MethodNotAllowedException" /> class. + /// </summary> + public MethodNotAllowedException() + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="MethodNotAllowedException" /> class. + /// </summary> + /// <param name="message">The message.</param> + public MethodNotAllowedException(string message) + : base(message) + { + } + } +} diff --git a/MediaBrowser.Common/Extensions/ProcessExtensions.cs b/MediaBrowser.Common/Extensions/ProcessExtensions.cs new file mode 100644 index 000000000..08e01bfd6 --- /dev/null +++ b/MediaBrowser.Common/Extensions/ProcessExtensions.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.Extensions +{ + /// <summary> + /// Extension methods for <see cref="Process"/>. + /// </summary> + public static class ProcessExtensions + { + /// <summary> + /// Asynchronously wait for the process to exit. + /// </summary> + /// <param name="process">The process to wait for.</param> + /// <param name="timeout">The duration to wait before cancelling waiting for the task.</param> + /// <returns>True if the task exited normally, false if the timeout elapsed before the process exited.</returns> + /// <exception cref="InvalidOperationException">If <see cref="Process.EnableRaisingEvents"/> is not set to true for the process.</exception> + public static async Task<bool> WaitForExitAsync(this Process process, TimeSpan timeout) + { + using (var cancelTokenSource = new CancellationTokenSource(timeout)) + { + return await WaitForExitAsync(process, cancelTokenSource.Token).ConfigureAwait(false); + } + } + + /// <summary> + /// Asynchronously wait for the process to exit. + /// </summary> + /// <param name="process">The process to wait for.</param> + /// <param name="cancelToken">A <see cref="CancellationToken"/> to observe while waiting for the process to exit.</param> + /// <returns>True if the task exited normally, false if cancelled before the process exited.</returns> + public static async Task<bool> WaitForExitAsync(this Process process, CancellationToken cancelToken) + { + if (!process.EnableRaisingEvents) + { + throw new InvalidOperationException("EnableRisingEvents must be enabled to async wait for a task to exit."); + } + + // Add an event handler for the process exit event + var tcs = new TaskCompletionSource<bool>(); + process.Exited += (_, _) => tcs.TrySetResult(true); + + // Return immediately if the process has already exited + if (process.HasExitedSafe()) + { + return true; + } + + // Register with the cancellation token then await + using (var cancelRegistration = cancelToken.Register(() => tcs.TrySetResult(process.HasExitedSafe()))) + { + return await tcs.Task.ConfigureAwait(false); + } + } + + /// <summary> + /// Gets a value indicating whether the associated process has been terminated using + /// <see cref="Process.HasExited"/>. This is safe to call even if there is no operating system process + /// associated with the <see cref="Process"/>. + /// </summary> + /// <param name="process">The process to check the exit status for.</param> + /// <returns> + /// True if the operating system process referenced by the <see cref="Process"/> component has + /// terminated, or if there is no associated operating system process; otherwise, false. + /// </returns> + private static bool HasExitedSafe(this Process process) + { + try + { + return process.HasExited; + } + catch (InvalidOperationException) + { + return true; + } + } + } +} diff --git a/MediaBrowser.Common/Extensions/RateLimitExceededException.cs b/MediaBrowser.Common/Extensions/RateLimitExceededException.cs new file mode 100644 index 000000000..95802a462 --- /dev/null +++ b/MediaBrowser.Common/Extensions/RateLimitExceededException.cs @@ -0,0 +1,25 @@ +#pragma warning disable CS1591 + +using System; + +namespace MediaBrowser.Common.Extensions +{ + public class RateLimitExceededException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="RateLimitExceededException" /> class. + /// </summary> + public RateLimitExceededException() + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="RateLimitExceededException" /> class. + /// </summary> + /// <param name="message">The message.</param> + public RateLimitExceededException(string message) + : base(message) + { + } + } +} diff --git a/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs index f62c65fd7..22130c5a1 100644 --- a/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs +++ b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs @@ -3,7 +3,7 @@ using System; namespace MediaBrowser.Common.Extensions { /// <summary> - /// Class ResourceNotFoundException + /// Class ResourceNotFoundException. /// </summary> public class ResourceNotFoundException : Exception { @@ -12,7 +12,6 @@ namespace MediaBrowser.Common.Extensions /// </summary> public ResourceNotFoundException() { - } /// <summary> @@ -22,42 +21,6 @@ namespace MediaBrowser.Common.Extensions public ResourceNotFoundException(string message) : base(message) { - - } - } - - public class RemoteServiceUnavailableException : Exception - { - public RemoteServiceUnavailableException() - { - - } - - public RemoteServiceUnavailableException(string message) - : base(message) - { - - } - } - - public class RateLimitExceededException : Exception - { - /// <summary> - /// Initializes a new instance of the <see cref="RateLimitExceededException" /> class. - /// </summary> - public RateLimitExceededException() - { - - } - - /// <summary> - /// Initializes a new instance of the <see cref="RateLimitExceededException" /> class. - /// </summary> - /// <param name="message">The message.</param> - public RateLimitExceededException(string message) - : base(message) - { - } } } diff --git a/MediaBrowser.Common/FfmpegException.cs b/MediaBrowser.Common/FfmpegException.cs new file mode 100644 index 000000000..be420196d --- /dev/null +++ b/MediaBrowser.Common/FfmpegException.cs @@ -0,0 +1,39 @@ +using System; + +namespace MediaBrowser.Common +{ + /// <summary> + /// Represents errors that occur during interaction with FFmpeg. + /// </summary> + public class FfmpegException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="FfmpegException"/> class. + /// </summary> + public FfmpegException() + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="FfmpegException"/> class with a specified error message. + /// </summary> + /// <param name="message">The message that describes the error.</param> + public FfmpegException(string message) : base(message) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="FfmpegException"/> class with a specified error message and a + /// reference to the inner exception that is the cause of this exception. + /// </summary> + /// <param name="message">The error message that explains the reason for the exception.</param> + /// <param name="innerException"> + /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if + /// no inner exception is specified. + /// </param> + public FfmpegException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index 3a4098612..192a77611 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -1,20 +1,28 @@ using System; using System.Collections.Generic; -using System.Threading; +using System.Reflection; using System.Threading.Tasks; -using MediaBrowser.Common.Plugins; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Updates; -using Microsoft.Extensions.DependencyInjection; namespace MediaBrowser.Common { /// <summary> - /// An interface to be implemented by the applications hosting a kernel + /// Delegate used with GetExports{T}. + /// </summary> + /// <param name="type">Type to create.</param> + /// <returns>New instance of type <param>type</param>.</returns> + public delegate object? CreationDelegateFactory(Type type); + + /// <summary> + /// An interface to be implemented by the applications hosting a kernel. /// </summary> public interface IApplicationHost { /// <summary> + /// Occurs when [has pending restart changed]. + /// </summary> + event EventHandler? HasPendingRestartChanged; + + /// <summary> /// Gets the name. /// </summary> /// <value>The name.</value> @@ -27,16 +35,15 @@ namespace MediaBrowser.Common string SystemId { get; } /// <summary> - /// Occurs when [application updated]. - /// </summary> - event EventHandler<GenericEventArgs<PackageVersionInfo>> ApplicationUpdated; - - /// <summary> - /// Gets or sets a value indicating whether this instance has pending kernel reload. + /// Gets a value indicating whether this instance has pending kernel reload. /// </summary> /// <value><c>true</c> if this instance has pending kernel reload; otherwise, <c>false</c>.</value> bool HasPendingRestart { get; } + /// <summary> + /// Gets a value indicating whether this instance is currently shutting down. + /// </summary> + /// <value><c>true</c> if this instance is shutting down; otherwise, <c>false</c>.</value> bool IsShuttingDown { get; } /// <summary> @@ -46,25 +53,21 @@ namespace MediaBrowser.Common bool CanSelfRestart { get; } /// <summary> - /// Occurs when [has pending restart changed]. - /// </summary> - event EventHandler HasPendingRestartChanged; - - /// <summary> - /// Notifies the pending restart. + /// Gets the application version. /// </summary> - void NotifyPendingRestart(); + /// <value>The application version.</value> + Version ApplicationVersion { get; } /// <summary> - /// Restarts this instance. + /// Gets or sets the service provider. /// </summary> - void Restart(); + IServiceProvider? ServiceProvider { get; set; } /// <summary> /// Gets the application version. /// </summary> /// <value>The application version.</value> - string ApplicationVersion { get; } + string ApplicationVersionString { get; } /// <summary> /// Gets the application user agent. @@ -73,41 +76,68 @@ namespace MediaBrowser.Common string ApplicationUserAgent { get; } /// <summary> + /// Gets the email address for use within a comment section of a user agent field. + /// Presently used to provide contact information to MusicBrainz service. + /// </summary> + string ApplicationUserAgentAddress { get; } + + /// <summary> + /// Gets all plugin assemblies which implement a custom rest api. + /// </summary> + /// <returns>An <see cref="IEnumerable{Assembly}"/> containing the plugin assemblies.</returns> + IEnumerable<Assembly> GetApiPluginAssemblies(); + + /// <summary> + /// Notifies the pending restart. + /// </summary> + void NotifyPendingRestart(); + + /// <summary> + /// Restarts this instance. + /// </summary> + void Restart(); + + /// <summary> /// Gets the exports. /// </summary> - /// <typeparam name="T"></typeparam> - /// <param name="manageLiftime">if set to <c>true</c> [manage liftime].</param> - /// <returns>IEnumerable{``0}.</returns> - IEnumerable<T> GetExports<T>(bool manageLifetime = true); + /// <typeparam name="T">The type.</typeparam> + /// <param name="manageLifetime">If set to <c>true</c> [manage lifetime].</param> + /// <returns><see cref="IReadOnlyCollection{T}" />.</returns> + IReadOnlyCollection<T> GetExports<T>(bool manageLifetime = true); /// <summary> - /// Resolves this instance. + /// Gets the exports. /// </summary> - /// <typeparam name="T"></typeparam> - /// <returns>``0.</returns> - T Resolve<T>(); + /// <typeparam name="T">The type.</typeparam> + /// <param name="defaultFunc">Delegate function that gets called to create the object.</param> + /// <param name="manageLifetime">If set to <c>true</c> [manage lifetime].</param> + /// <returns><see cref="IReadOnlyCollection{T}" />.</returns> + IReadOnlyCollection<T> GetExports<T>(CreationDelegateFactory defaultFunc, bool manageLifetime = true); /// <summary> - /// Shuts down. + /// Gets the export types. /// </summary> - Task Shutdown(); + /// <typeparam name="T">The type.</typeparam> + /// <returns>IEnumerable{Type}.</returns> + IEnumerable<Type> GetExportTypes<T>(); /// <summary> - /// Gets the plugins. + /// Resolves this instance. /// </summary> - /// <value>The plugins.</value> - IPlugin[] Plugins { get; } + /// <typeparam name="T">The <c>Type</c>.</typeparam> + /// <returns>``0.</returns> + T Resolve<T>(); /// <summary> - /// Removes the plugin. + /// Shuts down. /// </summary> - /// <param name="plugin">The plugin.</param> - void RemovePlugin(IPlugin plugin); + /// <returns>A task.</returns> + Task Shutdown(); /// <summary> - /// Inits this instance. + /// Initializes this instance. /// </summary> - Task Init(IServiceCollection serviceCollection); + void Init(); /// <summary> /// Creates the instance. @@ -115,7 +145,5 @@ namespace MediaBrowser.Common /// <param name="type">The type.</param> /// <returns>System.Object.</returns> object CreateInstance(Type type); - - PackageVersionClass SystemUpdateLevel { get; } } } diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 715f4fccd..9c8ce4ac5 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -1,18 +1,27 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{9142EEFA-7570-41E1-BFCC-468BB571AF2F}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <Authors>Jellyfin Contributors</Authors> <PackageId>Jellyfin.Common</PackageId> - <PackageLicenseUrl>https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt</PackageLicenseUrl> + <VersionPrefix>10.8.0</VersionPrefix> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> + <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> </PropertyGroup> <ItemGroup> + <FrameworkReference Include="Microsoft.AspNetCore.App" /> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.2.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> </ItemGroup> <ItemGroup> @@ -20,8 +29,31 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>net6.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <PublishRepositoryUrl>true</PublishRepositoryUrl> + <EmbedUntrackedSources>true</EmbedUntrackedSources> + <IncludeSymbols>true</IncludeSymbols> + <SymbolPackageFormat>snupkg</SymbolPackageFormat> + </PropertyGroup> + + <PropertyGroup Condition=" '$(Stability)'=='Unstable'"> + <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. --> + <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> </PropertyGroup> + <!-- Code analyzers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + + <ItemGroup> + <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo"> + <_Parameter1>Jellyfin.Common.Tests</_Parameter1> + </AssemblyAttribute> + </ItemGroup> + </Project> diff --git a/MediaBrowser.Common/Net/CustomHeaderNames.cs b/MediaBrowser.Common/Net/CustomHeaderNames.cs new file mode 100644 index 000000000..5ca9897eb --- /dev/null +++ b/MediaBrowser.Common/Net/CustomHeaderNames.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 + +namespace MediaBrowser.Common.Net +{ + public static class CustomHeaderNames + { + // Other Headers + public const string XForwardedFor = "X-Forwarded-For"; + public const string XForwardedPort = "X-Forwarded-Port"; + public const string XForwardedProto = "X-Forwarded-Proto"; + public const string XRealIP = "X-Real-IP"; + } +} diff --git a/MediaBrowser.Common/Net/HttpRequestOptions.cs b/MediaBrowser.Common/Net/HttpRequestOptions.cs deleted file mode 100644 index dadac5e03..000000000 --- a/MediaBrowser.Common/Net/HttpRequestOptions.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; - -namespace MediaBrowser.Common.Net -{ - /// <summary> - /// Class HttpRequestOptions - /// </summary> - public class HttpRequestOptions - { - /// <summary> - /// Gets or sets the URL. - /// </summary> - /// <value>The URL.</value> - public string Url { get; set; } - - public CompressionMethod? DecompressionMethod { get; set; } - - /// <summary> - /// Gets or sets the accept header. - /// </summary> - /// <value>The accept header.</value> - public string AcceptHeader - { - get => GetHeaderValue("Accept"); - set => RequestHeaders["Accept"] = value; - } - /// <summary> - /// Gets or sets the cancellation token. - /// </summary> - /// <value>The cancellation token.</value> - public CancellationToken CancellationToken { get; set; } - - /// <summary> - /// Gets or sets the resource pool. - /// </summary> - /// <value>The resource pool.</value> - public SemaphoreSlim ResourcePool { get; set; } - - /// <summary> - /// Gets or sets the user agent. - /// </summary> - /// <value>The user agent.</value> - public string UserAgent - { - get => GetHeaderValue("User-Agent"); - set => RequestHeaders["User-Agent"] = value; - } - - /// <summary> - /// Gets or sets the referrer. - /// </summary> - /// <value>The referrer.</value> - public string Referer { get; set; } - - /// <summary> - /// Gets or sets the host. - /// </summary> - /// <value>The host.</value> - public string Host { get; set; } - - /// <summary> - /// Gets or sets the progress. - /// </summary> - /// <value>The progress.</value> - public IProgress<double> Progress { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether [enable HTTP compression]. - /// </summary> - /// <value><c>true</c> if [enable HTTP compression]; otherwise, <c>false</c>.</value> - public bool EnableHttpCompression { get; set; } - - public Dictionary<string, string> RequestHeaders { get; private set; } - - public string RequestContentType { get; set; } - - public string RequestContent { get; set; } - public byte[] RequestContentBytes { get; set; } - - public bool BufferContent { get; set; } - - public bool LogRequest { get; set; } - public bool LogRequestAsDebug { get; set; } - public bool LogErrors { get; set; } - public bool LogResponse { get; set; } - public bool LogResponseHeaders { get; set; } - - public bool LogErrorResponseBody { get; set; } - public bool EnableKeepAlive { get; set; } - - public CacheMode CacheMode { get; set; } - public TimeSpan CacheLength { get; set; } - - public int TimeoutMs { get; set; } - public bool EnableDefaultUserAgent { get; set; } - - public bool AppendCharsetToMimeType { get; set; } - public string DownloadFilePath { get; set; } - - private string GetHeaderValue(string name) - { - RequestHeaders.TryGetValue(name, out var value); - - return value; - } - - /// <summary> - /// Initializes a new instance of the <see cref="HttpRequestOptions"/> class. - /// </summary> - public HttpRequestOptions() - { - EnableHttpCompression = true; - - RequestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - LogRequest = true; - LogErrors = true; - CacheMode = CacheMode.None; - - TimeoutMs = 20000; - } - - public void SetPostData(IDictionary<string, string> values) - { - var strings = values.Keys.Select(key => string.Format("{0}={1}", key, values[key])); - var postContent = string.Join("&", strings.ToArray()); - - RequestContent = postContent; - RequestContentType = "application/x-www-form-urlencoded"; - } - } - - public enum CacheMode - { - None = 0, - Unconditional = 1 - } - - public enum CompressionMethod - { - Deflate, - Gzip - } -} diff --git a/MediaBrowser.Common/Net/HttpResponseInfo.cs b/MediaBrowser.Common/Net/HttpResponseInfo.cs deleted file mode 100644 index 186674167..000000000 --- a/MediaBrowser.Common/Net/HttpResponseInfo.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; - -namespace MediaBrowser.Common.Net -{ - /// <summary> - /// Class HttpResponseInfo - /// </summary> - public class HttpResponseInfo : IDisposable - { - /// <summary> - /// Gets or sets the type of the content. - /// </summary> - /// <value>The type of the content.</value> - public string ContentType { get; set; } - - /// <summary> - /// Gets or sets the response URL. - /// </summary> - /// <value>The response URL.</value> - public string ResponseUrl { get; set; } - - /// <summary> - /// Gets or sets the content. - /// </summary> - /// <value>The content.</value> - public Stream Content { get; set; } - - /// <summary> - /// Gets or sets the status code. - /// </summary> - /// <value>The status code.</value> - public HttpStatusCode StatusCode { get; set; } - - /// <summary> - /// Gets or sets the temp file path. - /// </summary> - /// <value>The temp file path.</value> - public string TempFilePath { get; set; } - - /// <summary> - /// Gets or sets the length of the content. - /// </summary> - /// <value>The length of the content.</value> - public long? ContentLength { get; set; } - - /// <summary> - /// Gets or sets the headers. - /// </summary> - /// <value>The headers.</value> - public Dictionary<string, string> Headers { get; set; } - - private readonly IDisposable _disposable; - - public HttpResponseInfo(IDisposable disposable) - { - _disposable = disposable; - Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } - public HttpResponseInfo() - { - Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } - - public void Dispose() - { - if (_disposable != null) - { - _disposable.Dispose(); - } - } - } -} diff --git a/MediaBrowser.Common/Net/IHttpClient.cs b/MediaBrowser.Common/Net/IHttpClient.cs deleted file mode 100644 index 5aaf7e0be..000000000 --- a/MediaBrowser.Common/Net/IHttpClient.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.IO; -using System.Threading.Tasks; - -namespace MediaBrowser.Common.Net -{ - /// <summary> - /// Interface IHttpClient - /// </summary> - public interface IHttpClient - { - /// <summary> - /// Gets the response. - /// </summary> - /// <param name="options">The options.</param> - /// <returns>Task{HttpResponseInfo}.</returns> - Task<HttpResponseInfo> GetResponse(HttpRequestOptions options); - - /// <summary> - /// Gets the specified options. - /// </summary> - /// <param name="options">The options.</param> - /// <returns>Task{Stream}.</returns> - Task<Stream> Get(HttpRequestOptions options); - - /// <summary> - /// Sends the asynchronous. - /// </summary> - /// <param name="options">The options.</param> - /// <param name="httpMethod">The HTTP method.</param> - /// <returns>Task{HttpResponseInfo}.</returns> - Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod); - - /// <summary> - /// Posts the specified options. - /// </summary> - /// <param name="options">The options.</param> - /// <returns>Task{HttpResponseInfo}.</returns> - Task<HttpResponseInfo> Post(HttpRequestOptions options); - - /// <summary> - /// Downloads the contents of a given url into a temporary location - /// </summary> - /// <param name="options">The options.</param> - /// <returns>Task{System.String}.</returns> - /// <exception cref="System.ArgumentNullException">progress</exception> - /// <exception cref="Model.Net.HttpException"></exception> - Task<string> GetTempFile(HttpRequestOptions options); - - /// <summary> - /// Gets the temporary file response. - /// </summary> - /// <param name="options">The options.</param> - /// <returns>Task{HttpResponseInfo}.</returns> - Task<HttpResponseInfo> GetTempFileResponse(HttpRequestOptions options); - } -} diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 72fb6e2b8..b93939730 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -1,66 +1,238 @@ using System; using System.Collections.Generic; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; +using System.Collections.ObjectModel; +using System.Net; +using System.Net.NetworkInformation; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Common.Net { + /// <summary> + /// Interface for the NetworkManager class. + /// </summary> public interface INetworkManager { + /// <summary> + /// Event triggered on network changes. + /// </summary> event EventHandler NetworkChanged; /// <summary> - /// Gets a random port number that is currently available + /// Gets the published server urls list. + /// </summary> + Dictionary<IPNetAddress, string> PublishedServerUrls { get; } + + /// <summary> + /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. + /// </summary> + bool TrustAllIP6Interfaces { get; } + + /// <summary> + /// Gets the remote address filter. + /// </summary> + Collection<IPObject> RemoteAddressFilter { get; } + + /// <summary> + /// Gets or sets a value indicating whether iP6 is enabled. + /// </summary> + bool IsIP6Enabled { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether iP4 is enabled. + /// </summary> + bool IsIP4Enabled { get; set; } + + /// <summary> + /// Calculates the list of interfaces to use for Kestrel. + /// </summary> + /// <returns>A Collection{IPObject} object containing all the interfaces to bind. + /// If all the interfaces are specified, and none are excluded, it returns zero items + /// to represent any address.</returns> + /// <param name="individualInterfaces">When false, return <see cref="IPAddress.Any"/> or <see cref="IPAddress.IPv6Any"/> for all interfaces.</param> + Collection<IPObject> GetAllBindInterfaces(bool individualInterfaces = false); + + /// <summary> + /// Returns a collection containing the loopback interfaces. + /// </summary> + /// <returns>Collection{IPObject}.</returns> + Collection<IPObject> GetLoopbacks(); + + /// <summary> + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// The priority of selection is as follows:- + /// + /// The value contained in the startup parameter --published-server-url. + /// + /// If the user specified custom subnet overrides, the correct subnet for the source address. + /// + /// If the user specified bind interfaces to use:- + /// The bind interface that contains the source subnet. + /// The first bind interface specified that suits best first the source's endpoint. eg. external or internal. + /// + /// If the source is from a public subnet address range and the user hasn't specified any bind addresses:- + /// The first public interface that isn't a loopback and contains the source subnet. + /// The first public interface that isn't a loopback. Priority is given to interfaces with gateways. + /// An internal interface if there are no public ip addresses. + /// + /// If the source is from a private subnet address range and the user hasn't specified any bind addresses:- + /// The first private interface that contains the source subnet. + /// The first private interface that isn't a loopback. Priority is given to interfaces with gateways. + /// + /// If no interfaces meet any of these criteria, then a loopback address is returned. + /// + /// Interface that have been specifically excluded from binding are not used in any of the calculations. + /// </summary> + /// <param name="source">Source of the request.</param> + /// <param name="port">Optional port returned, if it's part of an override.</param> + /// <returns>IP Address to use, or loopback address if all else fails.</returns> + string GetBindInterface(IPObject source, out int? port); + + /// <summary> + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// (See <see cref="GetBindInterface(IPObject, out int?)"/>. + /// </summary> + /// <param name="source">Source of the request.</param> + /// <param name="port">Optional port returned, if it's part of an override.</param> + /// <returns>IP Address to use, or loopback address if all else fails.</returns> + string GetBindInterface(HttpRequest source, out int? port); + + /// <summary> + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// (See <see cref="GetBindInterface(IPObject, out int?)"/>. /// </summary> - /// <returns>System.Int32.</returns> - int GetRandomUnusedTcpPort(); + /// <param name="source">IP address of the request.</param> + /// <param name="port">Optional port returned, if it's part of an override.</param> + /// <returns>IP Address to use, or loopback address if all else fails.</returns> + string GetBindInterface(IPAddress source, out int? port); - int GetRandomUnusedUdpPort(); + /// <summary> + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// (See <see cref="GetBindInterface(IPObject, out int?)"/>. + /// </summary> + /// <param name="source">Source of the request.</param> + /// <param name="port">Optional port returned, if it's part of an override.</param> + /// <returns>IP Address to use, or loopback address if all else fails.</returns> + string GetBindInterface(string source, out int? port); - Func<string[]> LocalSubnetsFn { get; set; } + /// <summary> + /// Checks to see if the ip address is specifically excluded in LocalNetworkAddresses. + /// </summary> + /// <param name="address">IP address to check.</param> + /// <returns>True if it is.</returns> + bool IsExcludedInterface(IPAddress address); /// <summary> - /// Returns MAC Address from first Network Card in Computer + /// Get a list of all the MAC addresses associated with active interfaces. /// </summary> - /// <returns>[string] MAC Address</returns> - List<string> GetMacAddresses(); + /// <returns>List of MAC addresses.</returns> + IReadOnlyCollection<PhysicalAddress> GetMacAddresses(); /// <summary> - /// Determines whether [is in private address space] [the specified endpoint]. + /// Checks to see if the IP Address provided matches an interface that has a gateway. /// </summary> - /// <param name="endpoint">The endpoint.</param> - /// <returns><c>true</c> if [is in private address space] [the specified endpoint]; otherwise, <c>false</c>.</returns> - bool IsInPrivateAddressSpace(string endpoint); + /// <param name="addressObj">IP to check. Can be an IPAddress or an IPObject.</param> + /// <returns>Result of the check.</returns> + bool IsGatewayInterface(IPObject? addressObj); /// <summary> - /// Gets the network shares. + /// Checks to see if the IP Address provided matches an interface that has a gateway. /// </summary> - /// <param name="path">The path.</param> - /// <returns>IEnumerable{NetworkShare}.</returns> - IEnumerable<NetworkShare> GetNetworkShares(string path); + /// <param name="addressObj">IP to check. Can be an IPAddress or an IPObject.</param> + /// <returns>Result of the check.</returns> + bool IsGatewayInterface(IPAddress? addressObj); /// <summary> - /// Gets available devices within the domain + /// Returns true if the address is a private address. + /// The configuration option TrustIP6Interfaces overrides this functions behaviour. /// </summary> - /// <returns>PC's in the Domain</returns> - IEnumerable<FileSystemEntryInfo> GetNetworkDevices(); + /// <param name="address">Address to check.</param> + /// <returns>True or False.</returns> + bool IsPrivateAddressRange(IPObject address); /// <summary> - /// Determines whether [is in local network] [the specified endpoint]. + /// Returns true if the address is part of the user defined LAN. + /// The configuration option TrustIP6Interfaces overrides this functions behaviour. /// </summary> - /// <param name="endpoint">The endpoint.</param> - /// <returns><c>true</c> if [is in local network] [the specified endpoint]; otherwise, <c>false</c>.</returns> - bool IsInLocalNetwork(string endpoint); + /// <param name="address">IP to check.</param> + /// <returns>True if endpoint is within the LAN range.</returns> + bool IsInLocalNetwork(string address); - IpAddressInfo[] GetLocalIpAddresses(); + /// <summary> + /// Returns true if the address is part of the user defined LAN. + /// The configuration option TrustIP6Interfaces overrides this functions behaviour. + /// </summary> + /// <param name="address">IP to check.</param> + /// <returns>True if endpoint is within the LAN range.</returns> + bool IsInLocalNetwork(IPObject address); - IpAddressInfo ParseIpAddress(string ipAddress); + /// <summary> + /// Returns true if the address is part of the user defined LAN. + /// The configuration option TrustIP6Interfaces overrides this functions behaviour. + /// </summary> + /// <param name="address">IP to check.</param> + /// <returns>True if endpoint is within the LAN range.</returns> + bool IsInLocalNetwork(IPAddress address); - bool TryParseIpAddress(string ipAddress, out IpAddressInfo ipAddressInfo); + /// <summary> + /// Attempts to convert the token to an IP address, permitting for interface descriptions and indexes. + /// eg. "eth1", or "TP-LINK Wireless USB Adapter". + /// </summary> + /// <param name="token">Token to parse.</param> + /// <param name="result">Resultant object's ip addresses, if successful.</param> + /// <returns>Success of the operation.</returns> + bool TryParseInterface(string token, out Collection<IPObject>? result); - Task<IpAddressInfo[]> GetHostAddressesAsync(string host); + /// <summary> + /// Parses an array of strings into a Collection{IPObject}. + /// </summary> + /// <param name="values">Values to parse.</param> + /// <param name="negated">When true, only include values beginning with !. When false, ignore ! values.</param> + /// <returns>IPCollection object containing the value strings.</returns> + Collection<IPObject> CreateIPCollection(string[] values, bool negated = false); + + /// <summary> + /// Returns all the internal Bind interface addresses. + /// </summary> + /// <returns>An internal list of interfaces addresses.</returns> + Collection<IPObject> GetInternalBindAddresses(); - bool IsAddressInSubnets(string addressString, string[] subnets); + /// <summary> + /// Checks to see if an IP address is still a valid interface address. + /// </summary> + /// <param name="address">IP address to check.</param> + /// <returns>True if it is.</returns> + bool IsValidInterfaceAddress(IPAddress address); + + /// <summary> + /// Returns true if the IP address is in the excluded list. + /// </summary> + /// <param name="ip">IP to check.</param> + /// <returns>True if excluded.</returns> + bool IsExcluded(IPAddress ip); + + /// <summary> + /// Returns true if the IP address is in the excluded list. + /// </summary> + /// <param name="ip">IP to check.</param> + /// <returns>True if excluded.</returns> + bool IsExcluded(EndPoint ip); + + /// <summary> + /// Gets the filtered LAN ip addresses. + /// </summary> + /// <param name="filter">Optional filter for the list.</param> + /// <returns>Returns a filtered list of LAN addresses.</returns> + Collection<IPObject> GetFilteredLANSubnets(Collection<IPObject>? filter = null); + + /// <summary> + /// Checks to see if <paramref name="remoteIp"/> has access. + /// </summary> + /// <param name="remoteIp">IP Address of client.</param> + /// <returns><b>True</b> if has access, otherwise <b>false</b>.</returns> + bool HasRemoteAccess(IPAddress remoteIp); } } diff --git a/MediaBrowser.Common/Net/IPHost.cs b/MediaBrowser.Common/Net/IPHost.cs new file mode 100644 index 000000000..1f125f2b1 --- /dev/null +++ b/MediaBrowser.Common/Net/IPHost.cs @@ -0,0 +1,441 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text.RegularExpressions; + +namespace MediaBrowser.Common.Net +{ + /// <summary> + /// Object that holds a host name. + /// </summary> + public class IPHost : IPObject + { + /// <summary> + /// Gets or sets timeout value before resolve required, in minutes. + /// </summary> + public const int Timeout = 30; + + /// <summary> + /// Represents an IPHost that has no value. + /// </summary> + public static readonly IPHost None = new IPHost(string.Empty, IPAddress.None); + + /// <summary> + /// Time when last resolved in ticks. + /// </summary> + private DateTime? _lastResolved = null; + + /// <summary> + /// Gets the IP Addresses, attempting to resolve the name, if there are none. + /// </summary> + private IPAddress[] _addresses; + + /// <summary> + /// Initializes a new instance of the <see cref="IPHost"/> class. + /// </summary> + /// <param name="name">Host name to assign.</param> + public IPHost(string name) + { + HostName = name ?? throw new ArgumentNullException(nameof(name)); + _addresses = Array.Empty<IPAddress>(); + Resolved = false; + } + + /// <summary> + /// Initializes a new instance of the <see cref="IPHost"/> class. + /// </summary> + /// <param name="name">Host name to assign.</param> + /// <param name="address">Address to assign.</param> + private IPHost(string name, IPAddress address) + { + HostName = name ?? throw new ArgumentNullException(nameof(name)); + _addresses = new IPAddress[] { address ?? throw new ArgumentNullException(nameof(address)) }; + Resolved = !address.Equals(IPAddress.None); + } + + /// <summary> + /// Gets or sets the object's first IP address. + /// </summary> + public override IPAddress Address + { + get + { + return ResolveHost() ? this[0] : IPAddress.None; + } + + set + { + // Not implemented, as a host's address is determined by DNS. + throw new NotImplementedException("The address of a host is determined by DNS."); + } + } + + /// <summary> + /// Gets or sets the object's first IP's subnet prefix. + /// The setter does nothing, but shouldn't raise an exception. + /// </summary> + public override byte PrefixLength + { + get => (byte)(ResolveHost() ? 128 : 32); + + // Not implemented, as a host object can only have a prefix length of 128 (IPv6) or 32 (IPv4) prefix length, + // which is automatically determined by it's IP type. Anything else is meaningless. + set => throw new NotImplementedException(); + } + + /// <summary> + /// Gets a value indicating whether the address has a value. + /// </summary> + public bool HasAddress => _addresses.Length != 0; + + /// <summary> + /// Gets the host name of this object. + /// </summary> + public string HostName { get; } + + /// <summary> + /// Gets a value indicating whether this host has attempted to be resolved. + /// </summary> + public bool Resolved { get; private set; } + + /// <summary> + /// Gets or sets the IP Addresses associated with this object. + /// </summary> + /// <param name="index">Index of address.</param> + public IPAddress this[int index] + { + get + { + ResolveHost(); + return index >= 0 && index < _addresses.Length ? _addresses[index] : IPAddress.None; + } + } + + /// <summary> + /// Attempts to parse the host string. + /// </summary> + /// <param name="host">Host name to parse.</param> + /// <param name="hostObj">Object representing the string, if it has successfully been parsed.</param> + /// <returns><c>true</c> if the parsing is successful, <c>false</c> if not.</returns> + public static bool TryParse(string host, out IPHost hostObj) + { + if (string.IsNullOrWhiteSpace(host)) + { + hostObj = IPHost.None; + return false; + } + + // See if it's an IPv6 with port address e.g. [::1] or [::1]:120. + int i = host.IndexOf(']', StringComparison.Ordinal); + if (i != -1) + { + return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj); + } + + if (IPNetAddress.TryParse(host, out var netAddress)) + { + // Host name is an ip address, so fake resolve. + hostObj = new IPHost(host, netAddress.Address); + return true; + } + + // Is it a host, IPv4/6 with/out port? + string[] hosts = host.Split(':'); + + if (hosts.Length <= 2) + { + // This is either a hostname: port, or an IP4:port. + host = hosts[0]; + + if (string.Equals("localhost", host, StringComparison.OrdinalIgnoreCase)) + { + hostObj = new IPHost(host); + return true; + } + + if (IPAddress.TryParse(host, out var netIP)) + { + // Host name is an ip address, so fake resolve. + hostObj = new IPHost(host, netIP); + return true; + } + } + else + { + // Invalid host name, as it cannot contain : + hostObj = new IPHost(string.Empty, IPAddress.None); + return false; + } + + // Use regular expression as CheckHostName isn't RFC5892 compliant. + // Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation + string pattern = @"(?im)^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)$"; + + if (Regex.IsMatch(host, pattern)) + { + hostObj = new IPHost(host); + return true; + } + + hostObj = IPHost.None; + return false; + } + + /// <summary> + /// Attempts to parse the host string. + /// </summary> + /// <param name="host">Host name to parse.</param> + /// <returns>Object representing the string, if it has successfully been parsed.</returns> + public static IPHost Parse(string host) + { + if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res)) + { + return res; + } + + throw new InvalidCastException($"Host does not contain a valid value. {host}"); + } + + /// <summary> + /// Attempts to parse the host string, ensuring that it resolves only to a specific IP type. + /// </summary> + /// <param name="host">Host name to parse.</param> + /// <param name="family">Addressfamily filter.</param> + /// <returns>Object representing the string, if it has successfully been parsed.</returns> + public static IPHost Parse(string host, AddressFamily family) + { + if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res)) + { + if (family == AddressFamily.InterNetwork) + { + res.Remove(AddressFamily.InterNetworkV6); + } + else + { + res.Remove(AddressFamily.InterNetwork); + } + + return res; + } + + throw new InvalidCastException($"Host does not contain a valid value. {host}"); + } + + /// <summary> + /// Returns the Addresses that this item resolved to. + /// </summary> + /// <returns>IPAddress Array.</returns> + public IPAddress[] GetAddresses() + { + ResolveHost(); + return _addresses; + } + + /// <inheritdoc/> + public override bool Contains(IPAddress address) + { + if (address != null && !Address.Equals(IPAddress.None)) + { + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + foreach (var addr in GetAddresses()) + { + if (address.Equals(addr)) + { + return true; + } + } + } + + return false; + } + + /// <inheritdoc/> + public override bool Equals(IPObject? other) + { + if (other is IPHost otherObj) + { + // Do we have the name Hostname? + if (string.Equals(otherObj.HostName, HostName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (!ResolveHost() || !otherObj.ResolveHost()) + { + return false; + } + + // Do any of our IP addresses match? + foreach (IPAddress addr in _addresses) + { + foreach (IPAddress otherAddress in otherObj._addresses) + { + if (addr.Equals(otherAddress)) + { + return true; + } + } + } + } + + return false; + } + + /// <inheritdoc/> + public override bool IsIP6() + { + // Returns true if interfaces are only IP6. + if (ResolveHost()) + { + foreach (IPAddress i in _addresses) + { + if (i.AddressFamily != AddressFamily.InterNetworkV6) + { + return false; + } + } + + return true; + } + + return false; + } + + /// <inheritdoc/> + public override string ToString() + { + // StringBuilder not optimum here. + string output = string.Empty; + if (_addresses.Length > 0) + { + bool moreThanOne = _addresses.Length > 1; + if (moreThanOne) + { + output = "["; + } + + foreach (var i in _addresses) + { + if (Address.Equals(IPAddress.None) && Address.AddressFamily == AddressFamily.Unspecified) + { + output += HostName + ","; + } + else if (i.Equals(IPAddress.Any)) + { + output += "Any IP4 Address,"; + } + else if (Address.Equals(IPAddress.IPv6Any)) + { + output += "Any IP6 Address,"; + } + else if (i.Equals(IPAddress.Broadcast)) + { + output += "Any Address,"; + } + else if (i.AddressFamily == AddressFamily.InterNetwork) + { + output += $"{i}/32,"; + } + else + { + output += $"{i}/128,"; + } + } + + output = output[..^1]; + + if (moreThanOne) + { + output += "]"; + } + } + else + { + output = HostName; + } + + return output; + } + + /// <inheritdoc/> + public override void Remove(AddressFamily family) + { + if (ResolveHost()) + { + _addresses = _addresses.Where(p => p.AddressFamily != family).ToArray(); + } + } + + /// <inheritdoc/> + public override bool Contains(IPObject address) + { + // An IPHost cannot contain another IPObject, it can only be equal. + return Equals(address); + } + + /// <inheritdoc/> + protected override IPObject CalculateNetworkAddress() + { + var (address, prefixLength) = NetworkAddressOf(this[0], PrefixLength); + return new IPNetAddress(address, prefixLength); + } + + /// <summary> + /// Attempt to resolve the ip address of a host. + /// </summary> + /// <returns><c>true</c> if any addresses have been resolved, otherwise <c>false</c>.</returns> + private bool ResolveHost() + { + // When was the last time we resolved? + _lastResolved ??= DateTime.UtcNow; + + // If we haven't resolved before, or our timer has run out... + if ((_addresses.Length == 0 && !Resolved) || (DateTime.UtcNow > _lastResolved.Value.AddMinutes(Timeout))) + { + _lastResolved = DateTime.UtcNow; + ResolveHostInternal(); + Resolved = true; + } + + return _addresses.Length > 0; + } + + /// <summary> + /// Task that looks up a Host name and returns its IP addresses. + /// </summary> + private void ResolveHostInternal() + { + var hostName = HostName; + if (string.IsNullOrEmpty(hostName)) + { + return; + } + + // Resolves the host name - so save a DNS lookup. + if (string.Equals(hostName, "localhost", StringComparison.OrdinalIgnoreCase)) + { + _addresses = new IPAddress[] { IPAddress.Loopback, IPAddress.IPv6Loopback }; + return; + } + + if (Uri.CheckHostName(hostName) == UriHostNameType.Dns) + { + try + { + _addresses = Dns.GetHostEntry(hostName).AddressList; + } + catch (SocketException ex) + { + // Log and then ignore socket errors, as the result value will just be an empty array. + Debug.WriteLine("GetHostAddresses failed with {Message}.", ex.Message); + } + } + } + } +} diff --git a/MediaBrowser.Common/Net/IPNetAddress.cs b/MediaBrowser.Common/Net/IPNetAddress.cs new file mode 100644 index 000000000..f6e3971bf --- /dev/null +++ b/MediaBrowser.Common/Net/IPNetAddress.cs @@ -0,0 +1,276 @@ +using System; +using System.Net; +using System.Net.Sockets; + +namespace MediaBrowser.Common.Net +{ + /// <summary> + /// An object that holds and IP address and subnet mask. + /// </summary> + public class IPNetAddress : IPObject + { + /// <summary> + /// Represents an IPNetAddress that has no value. + /// </summary> + public static readonly IPNetAddress None = new IPNetAddress(IPAddress.None); + + /// <summary> + /// IPv4 multicast address. + /// </summary> + public static readonly IPAddress SSDPMulticastIPv4 = IPAddress.Parse("239.255.255.250"); + + /// <summary> + /// IPv6 local link multicast address. + /// </summary> + public static readonly IPAddress SSDPMulticastIPv6LinkLocal = IPAddress.Parse("ff02::C"); + + /// <summary> + /// IPv6 site local multicast address. + /// </summary> + public static readonly IPAddress SSDPMulticastIPv6SiteLocal = IPAddress.Parse("ff05::C"); + + /// <summary> + /// IP4Loopback address host. + /// </summary> + public static readonly IPNetAddress IP4Loopback = IPNetAddress.Parse("127.0.0.1/8"); + + /// <summary> + /// IP6Loopback address host. + /// </summary> + public static readonly IPNetAddress IP6Loopback = new IPNetAddress(IPAddress.IPv6Loopback); + + /// <summary> + /// Object's IP address. + /// </summary> + private IPAddress _address; + + /// <summary> + /// Initializes a new instance of the <see cref="IPNetAddress"/> class. + /// </summary> + /// <param name="address">Address to assign.</param> + public IPNetAddress(IPAddress address) + { + _address = address ?? throw new ArgumentNullException(nameof(address)); + PrefixLength = (byte)(address.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); + } + + /// <summary> + /// Initializes a new instance of the <see cref="IPNetAddress"/> class. + /// </summary> + /// <param name="address">IP Address.</param> + /// <param name="prefixLength">Mask as a CIDR.</param> + public IPNetAddress(IPAddress address, byte prefixLength) + { + if (address?.IsIPv4MappedToIPv6 ?? throw new ArgumentNullException(nameof(address))) + { + _address = address.MapToIPv4(); + } + else + { + _address = address; + } + + PrefixLength = prefixLength; + } + + /// <summary> + /// Gets or sets the object's IP address. + /// </summary> + public override IPAddress Address + { + get + { + return _address; + } + + set + { + _address = value ?? IPAddress.None; + } + } + + /// <inheritdoc/> + public override byte PrefixLength { get; set; } + + /// <summary> + /// Try to parse the address and subnet strings into an IPNetAddress object. + /// </summary> + /// <param name="addr">IP address to parse. Can be CIDR or X.X.X.X notation.</param> + /// <param name="ip">Resultant object.</param> + /// <returns>True if the values parsed successfully. False if not, resulting in the IP being null.</returns> + public static bool TryParse(string addr, out IPNetAddress ip) + { + if (!string.IsNullOrEmpty(addr)) + { + addr = addr.Trim(); + + // Try to parse it as is. + if (IPAddress.TryParse(addr, out IPAddress? res)) + { + ip = new IPNetAddress(res); + return true; + } + + // Is it a network? + string[] tokens = addr.Split('/'); + + if (tokens.Length == 2) + { + tokens[0] = tokens[0].TrimEnd(); + tokens[1] = tokens[1].TrimStart(); + + if (IPAddress.TryParse(tokens[0], out res)) + { + // Is the subnet part a cidr? + if (byte.TryParse(tokens[1], out byte cidr)) + { + ip = new IPNetAddress(res, cidr); + return true; + } + + // Is the subnet in x.y.a.b form? + if (IPAddress.TryParse(tokens[1], out IPAddress? mask)) + { + ip = new IPNetAddress(res, MaskToCidr(mask)); + return true; + } + } + } + } + + ip = None; + return false; + } + + /// <summary> + /// Parses the string provided, throwing an exception if it is badly formed. + /// </summary> + /// <param name="addr">String to parse.</param> + /// <returns>IPNetAddress object.</returns> + public static IPNetAddress Parse(string addr) + { + if (TryParse(addr, out IPNetAddress o)) + { + return o; + } + + throw new ArgumentException("Unable to recognise object :" + addr); + } + + /// <inheritdoc/> + public override bool Contains(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + var (altAddress, altPrefix) = NetworkAddressOf(address, PrefixLength); + return NetworkAddress.Address.Equals(altAddress) && NetworkAddress.PrefixLength >= altPrefix; + } + + /// <inheritdoc/> + public override bool Contains(IPObject address) + { + if (address is IPHost addressObj && addressObj.HasAddress) + { + foreach (IPAddress addr in addressObj.GetAddresses()) + { + if (Contains(addr)) + { + return true; + } + } + } + else if (address is IPNetAddress netaddrObj) + { + // Have the same network address, but different subnets? + if (NetworkAddress.Address.Equals(netaddrObj.NetworkAddress.Address)) + { + return NetworkAddress.PrefixLength <= netaddrObj.PrefixLength; + } + + var altAddress = NetworkAddressOf(netaddrObj.Address, PrefixLength).address; + return NetworkAddress.Address.Equals(altAddress); + } + + return false; + } + + /// <inheritdoc/> + public override bool Equals(IPObject? other) + { + if (other is IPNetAddress otherObj && !Address.Equals(IPAddress.None) && !otherObj.Address.Equals(IPAddress.None)) + { + return Address.Equals(otherObj.Address) && + PrefixLength == otherObj.PrefixLength; + } + + return false; + } + + /// <inheritdoc/> + public override bool Equals(IPAddress ip) + { + if (ip != null && !ip.Equals(IPAddress.None) && !Address.Equals(IPAddress.None)) + { + return ip.Equals(Address); + } + + return false; + } + + /// <inheritdoc/> + public override string ToString() + { + return ToString(false); + } + + /// <summary> + /// Returns a textual representation of this object. + /// </summary> + /// <param name="shortVersion">Set to true, if the subnet is to be excluded as part of the address.</param> + /// <returns>String representation of this object.</returns> + public string ToString(bool shortVersion) + { + if (!Address.Equals(IPAddress.None)) + { + if (Address.Equals(IPAddress.Any)) + { + return "Any IP4 Address"; + } + + if (Address.Equals(IPAddress.IPv6Any)) + { + return "Any IP6 Address"; + } + + if (Address.Equals(IPAddress.Broadcast)) + { + return "Any Address"; + } + + if (shortVersion) + { + return Address.ToString(); + } + + return $"{Address}/{PrefixLength}"; + } + + return string.Empty; + } + + /// <inheritdoc/> + protected override IPObject CalculateNetworkAddress() + { + var (address, prefixLength) = NetworkAddressOf(_address, PrefixLength); + return new IPNetAddress(address, prefixLength); + } + } +} diff --git a/MediaBrowser.Common/Net/IPObject.cs b/MediaBrowser.Common/Net/IPObject.cs new file mode 100644 index 000000000..2612268fd --- /dev/null +++ b/MediaBrowser.Common/Net/IPObject.cs @@ -0,0 +1,395 @@ +using System; +using System.Net; +using System.Net.Sockets; + +namespace MediaBrowser.Common.Net +{ + /// <summary> + /// Base network object class. + /// </summary> + public abstract class IPObject : IEquatable<IPObject> + { + /// <summary> + /// The network address of this object. + /// </summary> + private IPObject? _networkAddress; + + /// <summary> + /// Gets or sets a user defined value that is associated with this object. + /// </summary> + public int Tag { get; set; } + + /// <summary> + /// Gets or sets the object's IP address. + /// </summary> + public abstract IPAddress Address { get; set; } + + /// <summary> + /// Gets the object's network address. + /// </summary> + public IPObject NetworkAddress => _networkAddress ??= CalculateNetworkAddress(); + + /// <summary> + /// Gets or sets the object's IP address. + /// </summary> + public abstract byte PrefixLength { get; set; } + + /// <summary> + /// Gets the AddressFamily of this object. + /// </summary> + public AddressFamily AddressFamily + { + get + { + // Keep terms separate as Address performs other functions in inherited objects. + IPAddress address = Address; + return address.Equals(IPAddress.None) ? AddressFamily.Unspecified : address.AddressFamily; + } + } + + /// <summary> + /// Returns the network address of an object. + /// </summary> + /// <param name="address">IP Address to convert.</param> + /// <param name="prefixLength">Subnet prefix.</param> + /// <returns>IPAddress.</returns> + public static (IPAddress address, byte prefixLength) NetworkAddressOf(IPAddress address, byte prefixLength) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + if (IsLoopback(address)) + { + return (address, prefixLength); + } + + // An ip address is just a list of bytes, each one representing a segment on the network. + // This separates the IP address into octets and calculates how many octets will need to be altered or set to zero dependant upon the + // prefix length value. eg. /16 on a 4 octet ip4 address (192.168.2.240) will result in the 2 and the 240 being zeroed out. + // Where there is not an exact boundary (eg /23), mod is used to calculate how many bits of this value are to be kept. + + // GetAddressBytes + Span<byte> addressBytes = stackalloc byte[address.AddressFamily == AddressFamily.InterNetwork ? 4 : 16]; + address.TryWriteBytes(addressBytes, out _); + + int div = prefixLength / 8; + int mod = prefixLength % 8; + if (mod != 0) + { + // Prefix length is counted right to left, so subtract 8 so we know how many bits to clear. + mod = 8 - mod; + + // Shift out the bits from the octet that we don't want, by moving right then back left. + addressBytes[div] = (byte)((int)addressBytes[div] >> mod << mod); + // Move on the next byte. + div++; + } + + // Blank out the remaining octets from mod + 1 to the end of the byte array. (192.168.2.240/16 becomes 192.168.0.0) + for (int octet = div; octet < addressBytes.Length; octet++) + { + addressBytes[octet] = 0; + } + + // Return the network address for the prefix. + return (new IPAddress(addressBytes), prefixLength); + } + + /// <summary> + /// Tests to see if the ip address is a Loopback address. + /// </summary> + /// <param name="address">Value to test.</param> + /// <returns>True if it is.</returns> + public static bool IsLoopback(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (!address.Equals(IPAddress.None)) + { + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + return address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback); + } + + return false; + } + + /// <summary> + /// Tests to see if the ip address is an IP6 address. + /// </summary> + /// <param name="address">Value to test.</param> + /// <returns>True if it is.</returns> + public static bool IsIP6(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + return !address.Equals(IPAddress.None) && (address.AddressFamily == AddressFamily.InterNetworkV6); + } + + /// <summary> + /// Tests to see if the address in the private address range. + /// </summary> + /// <param name="address">Object to test.</param> + /// <returns>True if it contains a private address.</returns> + public static bool IsPrivateAddressRange(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (!address.Equals(IPAddress.None)) + { + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + if (address.AddressFamily == AddressFamily.InterNetwork) + { + // GetAddressBytes + Span<byte> octet = stackalloc byte[4]; + address.TryWriteBytes(octet, out _); + + return (octet[0] == 10) + || (octet[0] == 172 && octet[1] >= 16 && octet[1] <= 31) // RFC1918 + || (octet[0] == 192 && octet[1] == 168) // RFC1918 + || (octet[0] == 127); // RFC1122 + } + else + { + // GetAddressBytes + Span<byte> octet = stackalloc byte[16]; + address.TryWriteBytes(octet, out _); + + uint word = (uint)(octet[0] << 8) + octet[1]; + + return (word >= 0xfe80 && word <= 0xfebf) // fe80::/10 :Local link. + || (word >= 0xfc00 && word <= 0xfdff); // fc00::/7 :Unique local address. + } + } + + return false; + } + + /// <summary> + /// Returns true if the IPAddress contains an IP6 Local link address. + /// </summary> + /// <param name="address">IPAddress object to check.</param> + /// <returns>True if it is a local link address.</returns> + /// <remarks> + /// See https://stackoverflow.com/questions/6459928/explain-the-instance-properties-of-system-net-ipaddress + /// it appears that the IPAddress.IsIPv6LinkLocal is out of date. + /// </remarks> + public static bool IsIPv6LinkLocal(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + if (address.AddressFamily != AddressFamily.InterNetworkV6) + { + return false; + } + + // GetAddressBytes + Span<byte> octet = stackalloc byte[16]; + address.TryWriteBytes(octet, out _); + uint word = (uint)(octet[0] << 8) + octet[1]; + + return word >= 0xfe80 && word <= 0xfebf; // fe80::/10 :Local link. + } + + /// <summary> + /// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only. + /// </summary> + /// <param name="cidr">Subnet mask in CIDR notation.</param> + /// <param name="family">IPv4 or IPv6 family.</param> + /// <returns>String value of the subnet mask in dotted decimal notation.</returns> + public static IPAddress CidrToMask(byte cidr, AddressFamily family) + { + uint addr = 0xFFFFFFFF << (family == AddressFamily.InterNetwork ? 32 : 128 - cidr); + addr = ((addr & 0xff000000) >> 24) + | ((addr & 0x00ff0000) >> 8) + | ((addr & 0x0000ff00) << 8) + | ((addr & 0x000000ff) << 24); + return new IPAddress(addr); + } + + /// <summary> + /// Convert a mask to a CIDR. IPv4 only. + /// https://stackoverflow.com/questions/36954345/get-cidr-from-netmask. + /// </summary> + /// <param name="mask">Subnet mask.</param> + /// <returns>Byte CIDR representing the mask.</returns> + public static byte MaskToCidr(IPAddress mask) + { + if (mask == null) + { + throw new ArgumentNullException(nameof(mask)); + } + + byte cidrnet = 0; + if (!mask.Equals(IPAddress.Any)) + { + // GetAddressBytes + Span<byte> bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? 4 : 16]; + mask.TryWriteBytes(bytes, out _); + + var zeroed = false; + for (var i = 0; i < bytes.Length; i++) + { + for (int v = bytes[i]; (v & 0xFF) != 0; v <<= 1) + { + if (zeroed) + { + // Invalid netmask. + return (byte)~cidrnet; + } + + if ((v & 0x80) == 0) + { + zeroed = true; + } + else + { + cidrnet++; + } + } + } + } + + return cidrnet; + } + + /// <summary> + /// Tests to see if this object is a Loopback address. + /// </summary> + /// <returns>True if it is.</returns> + public virtual bool IsLoopback() + { + return IsLoopback(Address); + } + + /// <summary> + /// Removes all addresses of a specific type from this object. + /// </summary> + /// <param name="family">Type of address to remove.</param> + public virtual void Remove(AddressFamily family) + { + // This method only performs a function in the IPHost implementation of IPObject. + } + + /// <summary> + /// Tests to see if this object is an IPv6 address. + /// </summary> + /// <returns>True if it is.</returns> + public virtual bool IsIP6() + { + return IsIP6(Address); + } + + /// <summary> + /// Returns true if this IP address is in the RFC private address range. + /// </summary> + /// <returns>True this object has a private address.</returns> + public virtual bool IsPrivateAddressRange() + { + return IsPrivateAddressRange(Address); + } + + /// <summary> + /// Compares this to the object passed as a parameter. + /// </summary> + /// <param name="ip">Object to compare to.</param> + /// <returns>Equality result.</returns> + public virtual bool Equals(IPAddress ip) + { + if (ip != null) + { + if (ip.IsIPv4MappedToIPv6) + { + ip = ip.MapToIPv4(); + } + + return !Address.Equals(IPAddress.None) && Address.Equals(ip); + } + + return false; + } + + /// <summary> + /// Compares this to the object passed as a parameter. + /// </summary> + /// <param name="other">Object to compare to.</param> + /// <returns>Equality result.</returns> + public virtual bool Equals(IPObject? other) + { + if (other != null) + { + return !Address.Equals(IPAddress.None) && Address.Equals(other.Address); + } + + return false; + } + + /// <summary> + /// Compares the address in this object and the address in the object passed as a parameter. + /// </summary> + /// <param name="address">Object's IP address to compare to.</param> + /// <returns>Comparison result.</returns> + public abstract bool Contains(IPObject address); + + /// <summary> + /// Compares the address in this object and the address in the object passed as a parameter. + /// </summary> + /// <param name="address">Object's IP address to compare to.</param> + /// <returns>Comparison result.</returns> + public abstract bool Contains(IPAddress address); + + /// <inheritdoc/> + public override int GetHashCode() + { + return Address.GetHashCode(); + } + + /// <inheritdoc/> + public override bool Equals(object? obj) + { + return Equals(obj as IPObject); + } + + /// <summary> + /// Calculates the network address of this object. + /// </summary> + /// <returns>Returns the network address of this object.</returns> + protected abstract IPObject CalculateNetworkAddress(); + } +} diff --git a/MediaBrowser.Common/Net/NamedClient.cs b/MediaBrowser.Common/Net/NamedClient.cs new file mode 100644 index 000000000..0f6161c32 --- /dev/null +++ b/MediaBrowser.Common/Net/NamedClient.cs @@ -0,0 +1,18 @@ +namespace MediaBrowser.Common.Net +{ + /// <summary> + /// Registered http client names. + /// </summary> + public static class NamedClient + { + /// <summary> + /// Gets the value for the default named http client. + /// </summary> + public const string Default = nameof(Default); + + /// <summary> + /// Gets the value for the MusicBrainz named http client. + /// </summary> + public const string MusicBrainz = nameof(MusicBrainz); + } +} diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs new file mode 100644 index 000000000..264bfacb4 --- /dev/null +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -0,0 +1,260 @@ +using System; +using System.Collections.ObjectModel; +using System.Net; + +namespace MediaBrowser.Common.Net +{ + /// <summary> + /// Defines the <see cref="NetworkExtensions" />. + /// </summary> + public static class NetworkExtensions + { + /// <summary> + /// Add an address to the collection. + /// </summary> + /// <param name="source">The <see cref="Collection{IPObject}"/>.</param> + /// <param name="ip">Item to add.</param> + public static void AddItem(this Collection<IPObject> source, IPAddress ip) + { + if (!source.ContainsAddress(ip)) + { + source.Add(new IPNetAddress(ip, 32)); + } + } + + /// <summary> + /// Adds a network to the collection. + /// </summary> + /// <param name="source">The <see cref="Collection{IPObject}"/>.</param> + /// <param name="item">Item to add.</param> + /// <param name="itemsAreNetworks">If <c>true</c> the values are treated as subnets. + /// If <b>false</b> items are addresses.</param> + public static void AddItem(this Collection<IPObject> source, IPObject item, bool itemsAreNetworks = true) + { + if (!source.ContainsAddress(item) || !itemsAreNetworks) + { + source.Add(item); + } + } + + /// <summary> + /// Converts this object to a string. + /// </summary> + /// <param name="source">The <see cref="Collection{IPObject}"/>.</param> + /// <returns>Returns a string representation of this object.</returns> + public static string AsString(this Collection<IPObject> source) + { + return $"[{string.Join(',', source)}]"; + } + + /// <summary> + /// Returns true if the collection contains an item with the ip address, + /// or the ip address falls within any of the collection's network ranges. + /// </summary> + /// <param name="source">The <see cref="Collection{IPObject}"/>.</param> + /// <param name="item">The item to look for.</param> + /// <returns>True if the collection contains the item.</returns> + public static bool ContainsAddress(this Collection<IPObject> source, IPAddress item) + { + if (source.Count == 0) + { + return false; + } + + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + if (item.IsIPv4MappedToIPv6) + { + item = item.MapToIPv4(); + } + + foreach (var i in source) + { + if (i.Contains(item)) + { + return true; + } + } + + return false; + } + + /// <summary> + /// Returns true if the collection contains an item with the ip address, + /// or the ip address falls within any of the collection's network ranges. + /// </summary> + /// <param name="source">The <see cref="Collection{IPObject}"/>.</param> + /// <param name="item">The item to look for.</param> + /// <returns>True if the collection contains the item.</returns> + public static bool ContainsAddress(this Collection<IPObject> source, IPObject item) + { + if (source.Count == 0) + { + return false; + } + + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + foreach (var i in source) + { + if (i.Contains(item)) + { + return true; + } + } + + return false; + } + + /// <summary> + /// Compares two Collection{IPObject} objects. The order is ignored. + /// </summary> + /// <param name="source">The <see cref="Collection{IPObject}"/>.</param> + /// <param name="dest">Item to compare to.</param> + /// <returns>True if both are equal.</returns> + public static bool Compare(this Collection<IPObject> source, Collection<IPObject> dest) + { + if (dest == null || source.Count != dest.Count) + { + return false; + } + + foreach (var sourceItem in source) + { + bool found = false; + foreach (var destItem in dest) + { + if (sourceItem.Equals(destItem)) + { + found = true; + break; + } + } + + if (!found) + { + return false; + } + } + + return true; + } + + /// <summary> + /// Returns a collection containing the subnets of this collection given. + /// </summary> + /// <param name="source">The <see cref="Collection{IPObject}"/>.</param> + /// <returns>Collection{IPObject} object containing the subnets.</returns> + public static Collection<IPObject> AsNetworks(this Collection<IPObject> source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + Collection<IPObject> res = new Collection<IPObject>(); + + foreach (IPObject i in source) + { + if (i is IPNetAddress nw) + { + // Add the subnet calculated from the interface address/mask. + var na = nw.NetworkAddress; + na.Tag = i.Tag; + res.AddItem(na); + } + else if (i is IPHost ipHost) + { + // Flatten out IPHost and add all its ip addresses. + foreach (var addr in ipHost.GetAddresses()) + { + IPNetAddress host = new IPNetAddress(addr) + { + Tag = i.Tag + }; + + res.AddItem(host); + } + } + } + + return res; + } + + /// <summary> + /// Excludes all the items from this list that are found in excludeList. + /// </summary> + /// <param name="source">The <see cref="Collection{IPObject}"/>.</param> + /// <param name="excludeList">Items to exclude.</param> + /// <param name="isNetwork">Collection is a network collection.</param> + /// <returns>A new collection, with the items excluded.</returns> + public static Collection<IPObject> Exclude(this Collection<IPObject> source, Collection<IPObject> excludeList, bool isNetwork) + { + if (source.Count == 0 || excludeList == null) + { + return new Collection<IPObject>(source); + } + + Collection<IPObject> results = new Collection<IPObject>(); + + bool found; + foreach (var outer in source) + { + found = false; + + foreach (var inner in excludeList) + { + if (outer.Equals(inner)) + { + found = true; + break; + } + } + + if (!found) + { + results.AddItem(outer, isNetwork); + } + } + + return results; + } + + /// <summary> + /// Returns all items that co-exist in this object and target. + /// </summary> + /// <param name="source">The <see cref="Collection{IPObject}"/>.</param> + /// <param name="target">Collection to compare with.</param> + /// <returns>A collection containing all the matches.</returns> + public static Collection<IPObject> ThatAreContainedInNetworks(this Collection<IPObject> source, Collection<IPObject> target) + { + if (source.Count == 0) + { + return new Collection<IPObject>(); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + Collection<IPObject> nc = new Collection<IPObject>(); + + foreach (IPObject i in source) + { + if (target.ContainsAddress(i)) + { + nc.AddItem(i); + } + } + + return nc; + } + } +} diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs index 1ff2e98ef..8972089a8 100644 --- a/MediaBrowser.Common/Plugins/BasePlugin.cs +++ b/MediaBrowser.Common/Plugins/BasePlugin.cs @@ -1,15 +1,19 @@ +#nullable disable + using System; using System.IO; -using MediaBrowser.Common.Configuration; +using System.Reflection; using MediaBrowser.Model.Plugins; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Common.Plugins { + /// <summary> + /// Provides a common base class for all plugins. + /// </summary> public abstract class BasePlugin : IPlugin, IPluginAssembly { /// <summary> - /// Gets the name of the plugin + /// Gets the name of the plugin. /// </summary> /// <value>The name.</value> public abstract string Name { get; } @@ -27,42 +31,53 @@ namespace MediaBrowser.Common.Plugins public virtual Guid Id { get; private set; } /// <summary> - /// Gets the plugin version + /// Gets the plugin version. /// </summary> /// <value>The version.</value> public Version Version { get; private set; } /// <summary> - /// Gets the path to the assembly file + /// Gets the path to the assembly file. /// </summary> /// <value>The assembly file path.</value> public string AssemblyFilePath { get; private set; } /// <summary> + /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed. + /// </summary> + /// <value>The data folder path.</value> + public string DataFolderPath { get; private set; } + + /// <summary> + /// Gets a value indicating whether the plugin can be uninstalled. + /// </summary> + public bool CanUninstall => !Path.GetDirectoryName(AssemblyFilePath) + .Equals(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), StringComparison.Ordinal); + + /// <summary> /// Gets the plugin info. /// </summary> /// <returns>PluginInfo.</returns> public virtual PluginInfo GetPluginInfo() { - var info = new PluginInfo - { - Name = Name, - Version = Version.ToString(), - Description = Description, - Id = Id.ToString() - }; + var info = new PluginInfo( + Name, + Version, + Description, + Id, + CanUninstall); return info; } /// <summary> - /// Called when just before the plugin is uninstalled from the server. + /// Called just before the plugin is uninstalled from the server. /// </summary> public virtual void OnUninstalling() { - } + /// <inheritdoc /> public void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion) { AssemblyFilePath = assemblyFilePath; @@ -70,180 +85,10 @@ namespace MediaBrowser.Common.Plugins Version = assemblyVersion; } + /// <inheritdoc /> public void SetId(Guid assemblyId) { Id = assemblyId; } - - /// <summary> - /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed - /// </summary> - /// <value>The data folder path.</value> - public string DataFolderPath { get; private set; } - } - - /// <summary> - /// Provides a common base class for all plugins - /// </summary> - /// <typeparam name="TConfigurationType">The type of the T configuration type.</typeparam> - public abstract class BasePlugin<TConfigurationType> : BasePlugin, IHasPluginConfiguration - where TConfigurationType : BasePluginConfiguration - { - /// <summary> - /// Gets the application paths. - /// </summary> - /// <value>The application paths.</value> - protected IApplicationPaths ApplicationPaths { get; private set; } - - /// <summary> - /// Gets the XML serializer. - /// </summary> - /// <value>The XML serializer.</value> - protected IXmlSerializer XmlSerializer { get; private set; } - - /// <summary> - /// Gets the type of configuration this plugin uses - /// </summary> - /// <value>The type of the configuration.</value> - public Type ConfigurationType => typeof(TConfigurationType); - - private Action<string> _directoryCreateFn; - public void SetStartupInfo(Action<string> directoryCreateFn) - { - // hack alert, until the .net core transition is complete - _directoryCreateFn = directoryCreateFn; - } - - /// <summary> - /// Gets the name the assembly file - /// </summary> - /// <value>The name of the assembly file.</value> - protected string AssemblyFileName => Path.GetFileName(AssemblyFilePath); - - /// <summary> - /// The _configuration sync lock - /// </summary> - private readonly object _configurationSyncLock = new object(); - /// <summary> - /// The _configuration - /// </summary> - private TConfigurationType _configuration; - /// <summary> - /// Gets the plugin's configuration - /// </summary> - /// <value>The configuration.</value> - public TConfigurationType Configuration - { - get - { - // Lazy load - if (_configuration == null) - { - lock (_configurationSyncLock) - { - if (_configuration == null) - { - _configuration = LoadConfiguration(); - } - } - } - return _configuration; - } - protected set => _configuration = value; - } - - private TConfigurationType LoadConfiguration() - { - var path = ConfigurationFilePath; - - try - { - return (TConfigurationType)XmlSerializer.DeserializeFromFile(typeof(TConfigurationType), path); - } - catch - { - return (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType)); - } - } - - /// <summary> - /// Gets the name of the configuration file. Subclasses should override - /// </summary> - /// <value>The name of the configuration file.</value> - public virtual string ConfigurationFileName => Path.ChangeExtension(AssemblyFileName, ".xml"); - - /// <summary> - /// Gets the full path to the configuration file - /// </summary> - /// <value>The configuration file path.</value> - public string ConfigurationFilePath => Path.Combine(ApplicationPaths.PluginConfigurationsPath, ConfigurationFileName); - - /// <summary> - /// Initializes a new instance of the <see cref="BasePlugin{TConfigurationType}" /> class. - /// </summary> - /// <param name="applicationPaths">The application paths.</param> - /// <param name="xmlSerializer">The XML serializer.</param> - protected BasePlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) - { - ApplicationPaths = applicationPaths; - XmlSerializer = xmlSerializer; - } - - /// <summary> - /// The _save lock - /// </summary> - private readonly object _configurationSaveLock = new object(); - - /// <summary> - /// Saves the current configuration to the file system - /// </summary> - public virtual void SaveConfiguration() - { - lock (_configurationSaveLock) - { - _directoryCreateFn(Path.GetDirectoryName(ConfigurationFilePath)); - - XmlSerializer.SerializeToFile(Configuration, ConfigurationFilePath); - } - } - - /// <summary> - /// Completely overwrites the current configuration with a new copy - /// Returns true or false indicating success or failure - /// </summary> - /// <param name="configuration">The configuration.</param> - /// <exception cref="ArgumentNullException">configuration</exception> - public virtual void UpdateConfiguration(BasePluginConfiguration configuration) - { - if (configuration == null) - { - throw new ArgumentNullException(nameof(configuration)); - } - - Configuration = (TConfigurationType)configuration; - - SaveConfiguration(); - } - - /// <summary> - /// Gets the plugin's configuration - /// </summary> - /// <value>The configuration.</value> - BasePluginConfiguration IHasPluginConfiguration.Configuration => Configuration; - - public override PluginInfo GetPluginInfo() - { - var info = base.GetPluginInfo(); - - info.ConfigurationFileName = ConfigurationFileName; - - return info; - } - } - - public interface IPluginAssembly - { - void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion); - void SetId(Guid assemblyId); } } diff --git a/MediaBrowser.Common/Plugins/BasePluginOfT.cs b/MediaBrowser.Common/Plugins/BasePluginOfT.cs new file mode 100644 index 000000000..afda83a7c --- /dev/null +++ b/MediaBrowser.Common/Plugins/BasePluginOfT.cs @@ -0,0 +1,205 @@ +#nullable disable +#pragma warning disable SA1649 // File name should match first type name + +using System; +using System.IO; +using System.Runtime.InteropServices; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Common.Plugins +{ + /// <summary> + /// Provides a common base class for all plugins. + /// </summary> + /// <typeparam name="TConfigurationType">The type of the T configuration type.</typeparam> + public abstract class BasePlugin<TConfigurationType> : BasePlugin, IHasPluginConfiguration + where TConfigurationType : BasePluginConfiguration + { + /// <summary> + /// The configuration sync lock. + /// </summary> + private readonly object _configurationSyncLock = new object(); + + /// <summary> + /// The configuration save lock. + /// </summary> + private readonly object _configurationSaveLock = new object(); + + /// <summary> + /// The configuration. + /// </summary> + private TConfigurationType _configuration; + + /// <summary> + /// Initializes a new instance of the <see cref="BasePlugin{TConfigurationType}" /> class. + /// </summary> + /// <param name="applicationPaths">The application paths.</param> + /// <param name="xmlSerializer">The XML serializer.</param> + protected BasePlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + { + ApplicationPaths = applicationPaths; + XmlSerializer = xmlSerializer; + + var assembly = GetType().Assembly; + var assemblyName = assembly.GetName(); + var assemblyFilePath = assembly.Location; + + var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath)); + if (Version != null && !Directory.Exists(dataFolderPath)) + { + // Try again with the version number appended to the folder name. + dataFolderPath += "_" + Version.ToString(); + } + + SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version); + + var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true); + if (idAttributes.Length > 0) + { + var attribute = (GuidAttribute)idAttributes[0]; + var assemblyId = new Guid(attribute.Value); + + SetId(assemblyId); + } + } + + /// <summary> + /// Gets the application paths. + /// </summary> + /// <value>The application paths.</value> + protected IApplicationPaths ApplicationPaths { get; private set; } + + /// <summary> + /// Gets the XML serializer. + /// </summary> + /// <value>The XML serializer.</value> + protected IXmlSerializer XmlSerializer { get; private set; } + + /// <summary> + /// Gets the type of configuration this plugin uses. + /// </summary> + /// <value>The type of the configuration.</value> + public Type ConfigurationType => typeof(TConfigurationType); + + /// <summary> + /// Gets or sets the event handler that is triggered when this configuration changes. + /// </summary> + public EventHandler<BasePluginConfiguration> ConfigurationChanged { get; set; } + + /// <summary> + /// Gets the name the assembly file. + /// </summary> + /// <value>The name of the assembly file.</value> + protected string AssemblyFileName => Path.GetFileName(AssemblyFilePath); + + /// <summary> + /// Gets or sets the plugin configuration. + /// </summary> + /// <value>The configuration.</value> + public TConfigurationType Configuration + { + get + { + // Lazy load + if (_configuration == null) + { + lock (_configurationSyncLock) + { + _configuration ??= LoadConfiguration(); + } + } + + return _configuration; + } + + protected set => _configuration = value; + } + + /// <summary> + /// Gets the name of the configuration file. Subclasses should override. + /// </summary> + /// <value>The name of the configuration file.</value> + public virtual string ConfigurationFileName => Path.ChangeExtension(AssemblyFileName, ".xml"); + + /// <summary> + /// Gets the full path to the configuration file. + /// </summary> + /// <value>The configuration file path.</value> + public string ConfigurationFilePath => Path.Combine(ApplicationPaths.PluginConfigurationsPath, ConfigurationFileName); + + /// <summary> + /// Gets the plugin configuration. + /// </summary> + /// <value>The configuration.</value> + BasePluginConfiguration IHasPluginConfiguration.Configuration => Configuration; + + /// <summary> + /// Saves the current configuration to the file system. + /// </summary> + /// <param name="config">Configuration to save.</param> + public virtual void SaveConfiguration(TConfigurationType config) + { + lock (_configurationSaveLock) + { + var folder = Path.GetDirectoryName(ConfigurationFilePath); + if (!Directory.Exists(folder)) + { + Directory.CreateDirectory(folder); + } + + XmlSerializer.SerializeToFile(config, ConfigurationFilePath); + } + } + + /// <summary> + /// Saves the current configuration to the file system. + /// </summary> + public virtual void SaveConfiguration() + { + SaveConfiguration(Configuration); + } + + /// <inheritdoc /> + public virtual void UpdateConfiguration(BasePluginConfiguration configuration) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + Configuration = (TConfigurationType)configuration; + + SaveConfiguration(Configuration); + + ConfigurationChanged?.Invoke(this, configuration); + } + + /// <inheritdoc /> + public override PluginInfo GetPluginInfo() + { + var info = base.GetPluginInfo(); + + info.ConfigurationFileName = ConfigurationFileName; + + return info; + } + + private TConfigurationType LoadConfiguration() + { + var path = ConfigurationFilePath; + + try + { + return (TConfigurationType)XmlSerializer.DeserializeFromFile(typeof(TConfigurationType), path); + } + catch + { + var config = (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType)); + SaveConfiguration(config); + return config; + } + } + } +} diff --git a/MediaBrowser.Common/Plugins/IHasPluginConfiguration.cs b/MediaBrowser.Common/Plugins/IHasPluginConfiguration.cs new file mode 100644 index 000000000..af9272caa --- /dev/null +++ b/MediaBrowser.Common/Plugins/IHasPluginConfiguration.cs @@ -0,0 +1,27 @@ +using System; +using MediaBrowser.Model.Plugins; + +namespace MediaBrowser.Common.Plugins +{ + /// <summary> + /// Defines the <see cref="IHasPluginConfiguration" />. + /// </summary> + public interface IHasPluginConfiguration + { + /// <summary> + /// Gets the type of configuration this plugin uses. + /// </summary> + Type ConfigurationType { get; } + + /// <summary> + /// Gets the plugin's configuration. + /// </summary> + BasePluginConfiguration Configuration { get; } + + /// <summary> + /// Completely overwrites the current configuration with a new copy. + /// </summary> + /// <param name="configuration">The configuration.</param> + void UpdateConfiguration(BasePluginConfiguration configuration); + } +} diff --git a/MediaBrowser.Common/Plugins/IPlugin.cs b/MediaBrowser.Common/Plugins/IPlugin.cs index 32527c299..01e0a536d 100644 --- a/MediaBrowser.Common/Plugins/IPlugin.cs +++ b/MediaBrowser.Common/Plugins/IPlugin.cs @@ -1,51 +1,52 @@ +#nullable disable + using System; using MediaBrowser.Model.Plugins; namespace MediaBrowser.Common.Plugins { /// <summary> - /// Interface IPlugin + /// Defines the <see cref="IPlugin" />. /// </summary> public interface IPlugin { /// <summary> - /// Gets the name of the plugin + /// Gets the name of the plugin. /// </summary> - /// <value>The name.</value> string Name { get; } /// <summary> - /// Gets the description. + /// Gets the Description. /// </summary> - /// <value>The description.</value> string Description { get; } /// <summary> /// Gets the unique id. /// </summary> - /// <value>The unique id.</value> Guid Id { get; } /// <summary> - /// Gets the plugin version + /// Gets the plugin version. /// </summary> - /// <value>The version.</value> Version Version { get; } /// <summary> - /// Gets the path to the assembly file + /// Gets the path to the assembly file. /// </summary> - /// <value>The assembly file path.</value> string AssemblyFilePath { get; } /// <summary> - /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed + /// Gets a value indicating whether the plugin can be uninstalled. + /// </summary> + bool CanUninstall { get; } + + /// <summary> + /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed. /// </summary> - /// <value>The data folder path.</value> string DataFolderPath { get; } /// <summary> - /// Gets the plugin info. + /// Gets the <see cref="PluginInfo"/>. /// </summary> /// <returns>PluginInfo.</returns> PluginInfo GetPluginInfo(); @@ -55,29 +56,4 @@ namespace MediaBrowser.Common.Plugins /// </summary> void OnUninstalling(); } - - public interface IHasPluginConfiguration - { - /// <summary> - /// Gets the type of configuration this plugin uses - /// </summary> - /// <value>The type of the configuration.</value> - Type ConfigurationType { get; } - - /// <summary> - /// Completely overwrites the current configuration with a new copy - /// Returns true or false indicating success or failure - /// </summary> - /// <param name="configuration">The configuration.</param> - /// <exception cref="ArgumentNullException">configuration</exception> - void UpdateConfiguration(BasePluginConfiguration configuration); - - /// <summary> - /// Gets the plugin's configuration - /// </summary> - /// <value>The configuration.</value> - BasePluginConfiguration Configuration { get; } - - void SetStartupInfo(Action<string> directoryCreateFn); - } } diff --git a/MediaBrowser.Common/Plugins/IPluginAssembly.cs b/MediaBrowser.Common/Plugins/IPluginAssembly.cs new file mode 100644 index 000000000..6df4fbb76 --- /dev/null +++ b/MediaBrowser.Common/Plugins/IPluginAssembly.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 + +using System; + +namespace MediaBrowser.Common.Plugins +{ + public interface IPluginAssembly + { + void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion); + + void SetId(Guid assemblyId); + } +} diff --git a/MediaBrowser.Common/Plugins/IPluginManager.cs b/MediaBrowser.Common/Plugins/IPluginManager.cs new file mode 100644 index 000000000..176bcbbd5 --- /dev/null +++ b/MediaBrowser.Common/Plugins/IPluginManager.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Updates; +using Microsoft.Extensions.DependencyInjection; + +namespace MediaBrowser.Common.Plugins +{ + /// <summary> + /// Defines the <see cref="IPluginManager" />. + /// </summary> + public interface IPluginManager + { + /// <summary> + /// Gets the Plugins. + /// </summary> + IReadOnlyList<LocalPlugin> Plugins { get; } + + /// <summary> + /// Creates the plugins. + /// </summary> + void CreatePlugins(); + + /// <summary> + /// Returns all the assemblies. + /// </summary> + /// <returns>An IEnumerable{Assembly}.</returns> + IEnumerable<Assembly> LoadAssemblies(); + + /// <summary> + /// Registers the plugin's services with the DI. + /// Note: DI is not yet instantiated yet. + /// </summary> + /// <param name="serviceCollection">A <see cref="ServiceCollection"/> instance.</param> + void RegisterServices(IServiceCollection serviceCollection); + + /// <summary> + /// Saves the manifest back to disk. + /// </summary> + /// <param name="manifest">The <see cref="PluginManifest"/> to save.</param> + /// <param name="path">The path where to save the manifest.</param> + /// <returns>True if successful.</returns> + bool SaveManifest(PluginManifest manifest, string path); + + /// <summary> + /// Generates a manifest from repository data. + /// </summary> + /// <param name="packageInfo">The <see cref="PackageInfo"/> used to generate a manifest.</param> + /// <param name="version">Version to be installed.</param> + /// <param name="path">The path where to save the manifest.</param> + /// <param name="status">Initial status of the plugin.</param> + /// <returns>True if successful.</returns> + Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status); + + /// <summary> + /// Imports plugin details from a folder. + /// </summary> + /// <param name="folder">Folder of the plugin.</param> + void ImportPluginFrom(string folder); + + /// <summary> + /// Disable the plugin. + /// </summary> + /// <param name="assembly">The <see cref="Assembly"/> of the plug to disable.</param> + void FailPlugin(Assembly assembly); + + /// <summary> + /// Disable the plugin. + /// </summary> + /// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param> + void DisablePlugin(LocalPlugin plugin); + + /// <summary> + /// Enables the plugin, disabling all other versions. + /// </summary> + /// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param> + void EnablePlugin(LocalPlugin plugin); + + /// <summary> + /// Attempts to find the plugin with and id of <paramref name="id"/>. + /// </summary> + /// <param name="id">Id of plugin.</param> + /// <param name="version">The version of the plugin to locate.</param> + /// <returns>A <see cref="LocalPlugin"/> if located, or null if not.</returns> + LocalPlugin? GetPlugin(Guid id, Version? version = null); + + /// <summary> + /// Removes the plugin. + /// </summary> + /// <param name="plugin">The plugin.</param> + /// <returns>Outcome of the operation.</returns> + bool RemovePlugin(LocalPlugin plugin); + } +} diff --git a/MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs b/MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs new file mode 100644 index 000000000..3afe874c5 --- /dev/null +++ b/MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs @@ -0,0 +1,19 @@ +namespace MediaBrowser.Common.Plugins +{ + using Microsoft.Extensions.DependencyInjection; + + /// <summary> + /// Defines the <see cref="IPluginServiceRegistrator" />. + /// </summary> + public interface IPluginServiceRegistrator + { + /// <summary> + /// Registers the plugin's services with the service collection. + /// </summary> + /// <remarks> + /// This interface is only used for service registration and requires a parameterless constructor. + /// </remarks> + /// <param name="serviceCollection">The service collection.</param> + void RegisterServices(IServiceCollection serviceCollection); + } +} diff --git a/MediaBrowser.Common/Plugins/LocalPlugin.cs b/MediaBrowser.Common/Plugins/LocalPlugin.cs new file mode 100644 index 000000000..4c8e2d504 --- /dev/null +++ b/MediaBrowser.Common/Plugins/LocalPlugin.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Plugins; + +namespace MediaBrowser.Common.Plugins +{ + /// <summary> + /// Local plugin class. + /// </summary> + public class LocalPlugin : IEquatable<LocalPlugin> + { + private readonly bool _supported; + private Version? _version; + + /// <summary> + /// Initializes a new instance of the <see cref="LocalPlugin"/> class. + /// </summary> + /// <param name="path">The plugin path.</param> + /// <param name="isSupported"><b>True</b> if Jellyfin supports this version of the plugin.</param> + /// <param name="manifest">The manifest record for this plugin, or null if one does not exist.</param> + public LocalPlugin(string path, bool isSupported, PluginManifest manifest) + { + Path = path; + DllFiles = Array.Empty<string>(); + _supported = isSupported; + Manifest = manifest; + } + + /// <summary> + /// Gets the plugin id. + /// </summary> + public Guid Id => Manifest.Id; + + /// <summary> + /// Gets the plugin name. + /// </summary> + public string Name => Manifest.Name; + + /// <summary> + /// Gets the plugin version. + /// </summary> + public Version Version + { + get + { + if (_version == null) + { + _version = Version.Parse(Manifest.Version); + } + + return _version; + } + } + + /// <summary> + /// Gets the plugin path. + /// </summary> + public string Path { get; } + + /// <summary> + /// Gets or sets the list of dll files for this plugin. + /// </summary> + public IReadOnlyList<string> DllFiles { get; set; } + + /// <summary> + /// Gets or sets the instance of this plugin. + /// </summary> + public IPlugin? Instance { get; set; } + + /// <summary> + /// Gets a value indicating whether Jellyfin supports this version of the plugin, and it's enabled. + /// </summary> + public bool IsEnabledAndSupported => _supported && Manifest.Status >= PluginStatus.Active; + + /// <summary> + /// Gets a value indicating whether the plugin has a manifest. + /// </summary> + public PluginManifest Manifest { get; } + + /// <summary> + /// Compare two <see cref="LocalPlugin"/>. + /// </summary> + /// <param name="a">The first item.</param> + /// <param name="b">The second item.</param> + /// <returns>Comparison result.</returns> + public static int Compare(LocalPlugin a, LocalPlugin b) + { + if (a == null || b == null) + { + throw new ArgumentNullException(a == null ? nameof(a) : nameof(b)); + } + + var compare = string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase); + + // Id is not equal but name is. + if (!a.Id.Equals(b.Id) && compare == 0) + { + compare = a.Id.CompareTo(b.Id); + } + + return compare == 0 ? a.Version.CompareTo(b.Version) : compare; + } + + /// <summary> + /// Returns the plugin information. + /// </summary> + /// <returns>A <see cref="PluginInfo"/> instance containing the information.</returns> + public PluginInfo GetPluginInfo() + { + var inst = Instance?.GetPluginInfo() ?? new PluginInfo(Manifest.Name, Version, Manifest.Description, Manifest.Id, true); + inst.Status = Manifest.Status; + inst.HasImage = !string.IsNullOrEmpty(Manifest.ImagePath); + return inst; + } + + /// <inheritdoc /> + public override bool Equals(object? obj) + { + return obj is LocalPlugin other && this.Equals(other); + } + + /// <inheritdoc /> + public override int GetHashCode() + { + return Name.GetHashCode(StringComparison.OrdinalIgnoreCase); + } + + /// <inheritdoc /> + public bool Equals(LocalPlugin? other) + { + if (other == null) + { + return false; + } + + return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) && Id.Equals(other.Id) && Version.Equals(other.Version); + } + } +} diff --git a/MediaBrowser.Common/Plugins/PluginManifest.cs b/MediaBrowser.Common/Plugins/PluginManifest.cs new file mode 100644 index 000000000..2910dbe14 --- /dev/null +++ b/MediaBrowser.Common/Plugins/PluginManifest.cs @@ -0,0 +1,108 @@ +using System; +using System.Text.Json.Serialization; +using MediaBrowser.Model.Plugins; + +namespace MediaBrowser.Common.Plugins +{ + /// <summary> + /// Defines a Plugin manifest file. + /// </summary> + public class PluginManifest + { + /// <summary> + /// Initializes a new instance of the <see cref="PluginManifest"/> class. + /// </summary> + public PluginManifest() + { + Category = string.Empty; + Changelog = string.Empty; + Description = string.Empty; + Id = Guid.Empty; + Name = string.Empty; + Owner = string.Empty; + Overview = string.Empty; + TargetAbi = string.Empty; + Version = string.Empty; + } + + /// <summary> + /// Gets or sets the category of the plugin. + /// </summary> + [JsonPropertyName("category")] + public string Category { get; set; } + + /// <summary> + /// Gets or sets the changelog information. + /// </summary> + [JsonPropertyName("changelog")] + public string Changelog { get; set; } + + /// <summary> + /// Gets or sets the description of the plugin. + /// </summary> + [JsonPropertyName("description")] + public string Description { get; set; } + + /// <summary> + /// Gets or sets the Global Unique Identifier for the plugin. + /// </summary> + [JsonPropertyName("guid")] + public Guid Id { get; set; } + + /// <summary> + /// Gets or sets the Name of the plugin. + /// </summary> + [JsonPropertyName("name")] + public string Name { get; set; } + + /// <summary> + /// Gets or sets an overview of the plugin. + /// </summary> + [JsonPropertyName("overview")] + public string Overview { get; set; } + + /// <summary> + /// Gets or sets the owner of the plugin. + /// </summary> + [JsonPropertyName("owner")] + public string Owner { get; set; } + + /// <summary> + /// Gets or sets the compatibility version for the plugin. + /// </summary> + [JsonPropertyName("targetAbi")] + public string TargetAbi { get; set; } + + /// <summary> + /// Gets or sets the timestamp of the plugin. + /// </summary> + [JsonPropertyName("timestamp")] + public DateTime Timestamp { get; set; } + + /// <summary> + /// Gets or sets the Version number of the plugin. + /// </summary> + [JsonPropertyName("version")] + public string Version { get; set; } + + /// <summary> + /// Gets or sets a value indicating the operational status of this plugin. + /// </summary> + [JsonPropertyName("status")] + public PluginStatus Status { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this plugin should automatically update. + /// </summary> + [JsonPropertyName("autoUpdate")] + public bool AutoUpdate { get; set; } = true; // DO NOT MOVE THIS INTO THE CONSTRUCTOR. + + /// <summary> + /// Gets or sets the ImagePath + /// Gets or sets a value indicating whether this plugin has an image. + /// Image must be located in the local plugin folder. + /// </summary> + [JsonPropertyName("imagePath")] + public string? ImagePath { get; set; } + } +} diff --git a/MediaBrowser.Common/Progress/ActionableProgress.cs b/MediaBrowser.Common/Progress/ActionableProgress.cs index 9fe01931f..0ba46ea3b 100644 --- a/MediaBrowser.Common/Progress/ActionableProgress.cs +++ b/MediaBrowser.Common/Progress/ActionableProgress.cs @@ -1,18 +1,22 @@ +#pragma warning disable CS1591 +#pragma warning disable CA1003 + using System; namespace MediaBrowser.Common.Progress { /// <summary> - /// Class ActionableProgress + /// Class ActionableProgress. /// </summary> - /// <typeparam name="T"></typeparam> + /// <typeparam name="T">The type for the action parameter.</typeparam> public class ActionableProgress<T> : IProgress<T> { /// <summary> - /// The _actions + /// The _actions. /// </summary> - private Action<T> _action; - public event EventHandler<T> ProgressChanged; + private Action<T>? _action; + + public event EventHandler<T>? ProgressChanged; /// <summary> /// Registers the action. @@ -25,29 +29,9 @@ namespace MediaBrowser.Common.Progress public void Report(T value) { - if (ProgressChanged != null) - { - ProgressChanged(this, value); - } - - var action = _action; - if (action != null) - { - action(value); - } - } - } + ProgressChanged?.Invoke(this, value); - public class SimpleProgress<T> : IProgress<T> - { - public event EventHandler<T> ProgressChanged; - - public void Report(T value) - { - if (ProgressChanged != null) - { - ProgressChanged(this, value); - } + _action?.Invoke(value); } } } diff --git a/MediaBrowser.Common/Progress/SimpleProgress.cs b/MediaBrowser.Common/Progress/SimpleProgress.cs new file mode 100644 index 000000000..7071f2bc3 --- /dev/null +++ b/MediaBrowser.Common/Progress/SimpleProgress.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS1591 +#pragma warning disable CA1003 + +using System; + +namespace MediaBrowser.Common.Progress +{ + public class SimpleProgress<T> : IProgress<T> + { + public event EventHandler<T>? ProgressChanged; + + public void Report(T value) + { + ProgressChanged?.Invoke(this, value); + } + } +} diff --git a/MediaBrowser.Common/Properties/AssemblyInfo.cs b/MediaBrowser.Common/Properties/AssemblyInfo.cs index 1a8fdb618..538e89fd1 100644 --- a/MediaBrowser.Common/Properties/AssemblyInfo.cs +++ b/MediaBrowser.Common/Properties/AssemblyInfo.cs @@ -9,8 +9,8 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] +[assembly: AssemblyProduct("Jellyfin Server")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/MediaBrowser.Common/Providers/ProviderIdParsers.cs b/MediaBrowser.Common/Providers/ProviderIdParsers.cs new file mode 100644 index 000000000..487b5a6d2 --- /dev/null +++ b/MediaBrowser.Common/Providers/ProviderIdParsers.cs @@ -0,0 +1,123 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace MediaBrowser.Common.Providers +{ + /// <summary> + /// Parsers for provider ids. + /// </summary> + public static class ProviderIdParsers + { + private const int ImdbMinNumbers = 7; + private const int ImdbMaxNumbers = 8; + private const string ImdbPrefix = "tt"; + + /// <summary> + /// Parses an IMDb id from a string. + /// </summary> + /// <param name="text">The text to parse.</param> + /// <param name="imdbId">The parsed IMDb id.</param> + /// <returns>True if parsing was successful, false otherwise.</returns> + public static bool TryFindImdbId(ReadOnlySpan<char> text, out ReadOnlySpan<char> imdbId) + { + // imdb id is at least 9 chars (tt + 7 numbers) + while (text.Length >= 2 + ImdbMinNumbers) + { + var ttPos = text.IndexOf(ImdbPrefix); + if (ttPos == -1) + { + imdbId = default; + return false; + } + + text = text.Slice(ttPos); + var i = 2; + var limit = Math.Min(text.Length, ImdbMaxNumbers + 2); + for (; i < limit; i++) + { + var c = text[i]; + if (!IsDigit(c)) + { + break; + } + } + + // skip if more than 8 digits + 2 chars for tt + if (i <= ImdbMaxNumbers + 2 && i >= ImdbMinNumbers + 2) + { + imdbId = text.Slice(0, i); + return true; + } + + text = text.Slice(i); + } + + imdbId = default; + return false; + } + + /// <summary> + /// Parses an TMDb id from a movie url. + /// </summary> + /// <param name="text">The text with the url to parse.</param> + /// <param name="tmdbId">The parsed TMDb id.</param> + /// <returns>True if parsing was successful, false otherwise.</returns> + public static bool TryFindTmdbMovieId(ReadOnlySpan<char> text, out ReadOnlySpan<char> tmdbId) + => TryFindProviderId(text, "themoviedb.org/movie/", out tmdbId); + + /// <summary> + /// Parses an TMDb id from a series url. + /// </summary> + /// <param name="text">The text with the url to parse.</param> + /// <param name="tmdbId">The parsed TMDb id.</param> + /// <returns>True if parsing was successful, false otherwise.</returns> + public static bool TryFindTmdbSeriesId(ReadOnlySpan<char> text, out ReadOnlySpan<char> tmdbId) + => TryFindProviderId(text, "themoviedb.org/tv/", out tmdbId); + + /// <summary> + /// Parses an TVDb id from a url. + /// </summary> + /// <param name="text">The text with the url to parse.</param> + /// <param name="tvdbId">The parsed TVDb id.</param> + /// <returns>True if parsing was successful, false otherwise.</returns> + public static bool TryFindTvdbId(ReadOnlySpan<char> text, out ReadOnlySpan<char> tvdbId) + => TryFindProviderId(text, "thetvdb.com/?tab=series&id=", out tvdbId); + + private static bool TryFindProviderId(ReadOnlySpan<char> text, ReadOnlySpan<char> searchString, [NotNullWhen(true)] out ReadOnlySpan<char> providerId) + { + var searchPos = text.IndexOf(searchString); + if (searchPos == -1) + { + providerId = default; + return false; + } + + text = text.Slice(searchPos + searchString.Length); + + int i = 0; + for (; i < text.Length; i++) + { + var c = text[i]; + + if (!IsDigit(c)) + { + break; + } + } + + if (i >= 1) + { + providerId = text.Slice(0, i); + return true; + } + + providerId = default; + return false; + } + + private static bool IsDigit(char c) + { + return c >= '0' && c <= '9'; + } + } +} diff --git a/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs b/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs new file mode 100644 index 000000000..0445397ad --- /dev/null +++ b/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs @@ -0,0 +1,21 @@ +#pragma warning disable CS1591 + +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Common.Providers +{ + public class SubtitleConfigurationFactory : IConfigurationFactory + { + /// <inheritdoc /> + public IEnumerable<ConfigurationStore> GetConfigurations() + { + yield return new ConfigurationStore() + { + Key = "subtitles", + ConfigurationType = typeof(SubtitleOptions) + }; + } + } +} diff --git a/MediaBrowser.Common/System/OperatingSystem.cs b/MediaBrowser.Common/System/OperatingSystem.cs new file mode 100644 index 000000000..5f673d320 --- /dev/null +++ b/MediaBrowser.Common/System/OperatingSystem.cs @@ -0,0 +1,74 @@ +#pragma warning disable CS1591 + +using System; +using System.Runtime.InteropServices; +using System.Threading; +using MediaBrowser.Model.System; + +namespace MediaBrowser.Common.System +{ + public static class OperatingSystem + { + // We can't use Interlocked.CompareExchange for enums + private static int _id = int.MaxValue; + + public static OperatingSystemId Id + { + get + { + if (_id == int.MaxValue) + { + Interlocked.CompareExchange(ref _id, (int)GetId(), int.MaxValue); + } + + return (OperatingSystemId)_id; + } + } + + public static string Name + { + get + { + switch (Id) + { + case OperatingSystemId.BSD: return "BSD"; + case OperatingSystemId.Linux: return "Linux"; + case OperatingSystemId.Darwin: return "macOS"; + case OperatingSystemId.Windows: return "Windows"; + default: throw new PlatformNotSupportedException($"Unknown OS {Id}"); + } + } + } + + private static OperatingSystemId GetId() + { + switch (Environment.OSVersion.Platform) + { + // On .NET Core `MacOSX` got replaced by `Unix`, this case should never be hit. + case PlatformID.MacOSX: + return OperatingSystemId.Darwin; + case PlatformID.Win32NT: + return OperatingSystemId.Windows; + case PlatformID.Unix: + default: + { + string osDescription = RuntimeInformation.OSDescription; + if (osDescription.Contains("linux", StringComparison.OrdinalIgnoreCase)) + { + return OperatingSystemId.Linux; + } + else if (osDescription.Contains("darwin", StringComparison.OrdinalIgnoreCase)) + { + return OperatingSystemId.Darwin; + } + else if (osDescription.Contains("bsd", StringComparison.OrdinalIgnoreCase)) + { + return OperatingSystemId.BSD; + } + + throw new PlatformNotSupportedException($"Can't resolve OS with description: '{osDescription}'"); + } + } + } + } +} diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs index a263be35f..458494bdc 100644 --- a/MediaBrowser.Common/Updates/IInstallationManager.cs +++ b/MediaBrowser.Common/Updates/IInstallationManager.cs @@ -3,117 +3,93 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Plugins; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Updates; namespace MediaBrowser.Common.Updates { + /// <summary> + /// Defines the <see cref="IInstallationManager" />. + /// </summary> public interface IInstallationManager : IDisposable { - event EventHandler<InstallationEventArgs> PackageInstalling; - event EventHandler<InstallationEventArgs> PackageInstallationCompleted; - event EventHandler<InstallationFailedEventArgs> PackageInstallationFailed; - event EventHandler<InstallationEventArgs> PackageInstallationCancelled; - - /// <summary> - /// The current installations - /// </summary> - List<Tuple<InstallationInfo, CancellationTokenSource>> CurrentInstallations { get; set; } - /// <summary> - /// The completed installations + /// Gets the completed installations. /// </summary> IEnumerable<InstallationInfo> CompletedInstallations { get; } /// <summary> - /// Occurs when [plugin uninstalled]. - /// </summary> - event EventHandler<GenericEventArgs<IPlugin>> PluginUninstalled; - - /// <summary> - /// Occurs when [plugin updated]. - /// </summary> - event EventHandler<GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>>> PluginUpdated; - - /// <summary> - /// Occurs when [plugin updated]. - /// </summary> - event EventHandler<GenericEventArgs<PackageVersionInfo>> PluginInstalled; - - /// <summary> - /// Gets all available packages. + /// Parses a plugin manifest at the supplied URL. /// </summary> + /// <param name="manifestName">Name of the repository.</param> + /// <param name="manifest">The URL to query.</param> + /// <param name="filterIncompatible">Filter out incompatible plugins.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <param name="withRegistration">if set to <c>true</c> [with registration].</param> - /// <param name="packageType">Type of the package.</param> - /// <param name="applicationVersion">The application version.</param> - /// <returns>Task{List{PackageInfo}}.</returns> - Task<List<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken, - bool withRegistration = true, string packageType = null, Version applicationVersion = null); + /// <returns>Task{IReadOnlyList{PackageInfo}}.</returns> + Task<PackageInfo[]> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default); /// <summary> - /// Gets all available packages from a static resource. + /// Gets all available packages that are supported by this version. /// </summary> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{List{PackageInfo}}.</returns> - Task<List<PackageInfo>> GetAvailablePackagesWithoutRegistrationInfo(CancellationToken cancellationToken); - - /// <summary> - /// Gets the package. - /// </summary> - /// <param name="name">The name.</param> - /// <param name="guid">The assembly guid</param> - /// <param name="classification">The classification.</param> - /// <param name="version">The version.</param> - /// <returns>Task{PackageVersionInfo}.</returns> - Task<PackageVersionInfo> GetPackage(string name, string guid, PackageVersionClass classification, Version version); + /// <returns>Task{IReadOnlyList{PackageInfo}}.</returns> + Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default); /// <summary> - /// Gets the latest compatible version. + /// Returns all plugins matching the requirements. /// </summary> - /// <param name="name">The name.</param> - /// <param name="guid">The assembly guid</param> - /// <param name="currentServerVersion">The current server version.</param> - /// <param name="classification">The classification.</param> - /// <returns>Task{PackageVersionInfo}.</returns> - Task<PackageVersionInfo> GetLatestCompatibleVersion(string name, string guid, Version currentServerVersion, PackageVersionClass classification = PackageVersionClass.Release); + /// <param name="availablePackages">The available packages.</param> + /// <param name="name">The name of the plugin.</param> + /// <param name="id">The id of the plugin.</param> + /// <param name="specificVersion">The version of the plugin.</param> + /// <returns>All plugins matching the requirements.</returns> + IEnumerable<PackageInfo> FilterPackages( + IEnumerable<PackageInfo> availablePackages, + string? name = null, + Guid id = default, + Version? specificVersion = null); /// <summary> - /// Gets the latest compatible version. + /// Returns all compatible versions ordered from newest to oldest. /// </summary> /// <param name="availablePackages">The available packages.</param> /// <param name="name">The name.</param> - /// <param name="guid">The assembly guid</param> - /// <param name="currentServerVersion">The current server version.</param> - /// <param name="classification">The classification.</param> - /// <returns>PackageVersionInfo.</returns> - PackageVersionInfo GetLatestCompatibleVersion(IEnumerable<PackageInfo> availablePackages, string name, string guid, Version currentServerVersion, PackageVersionClass classification = PackageVersionClass.Release); + /// <param name="id">The id of the plugin.</param> + /// <param name="minVersion">The minimum required version of the plugin.</param> + /// <param name="specificVersion">The specific version of the plugin to install.</param> + /// <returns>All compatible versions ordered from newest to oldest.</returns> + IEnumerable<InstallationInfo> GetCompatibleVersions( + IEnumerable<PackageInfo> availablePackages, + string? name = null, + Guid id = default, + Version? minVersion = null, + Version? specificVersion = null); /// <summary> - /// Gets the available plugin updates. + /// Returns the available compatible plugin updates. /// </summary> - /// <param name="applicationVersion">The current server version.</param> - /// <param name="withAutoUpdateEnabled">if set to <c>true</c> [with auto update enabled].</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{IEnumerable{PackageVersionInfo}}.</returns> - Task<IEnumerable<PackageVersionInfo>> GetAvailablePluginUpdates(Version applicationVersion, bool withAutoUpdateEnabled, CancellationToken cancellationToken); + /// <returns>The available plugin updates.</returns> + Task<IEnumerable<InstallationInfo>> GetAvailablePluginUpdates(CancellationToken cancellationToken = default); /// <summary> /// Installs the package. /// </summary> /// <param name="package">The package.</param> - /// <param name="isPlugin">if set to <c>true</c> [is plugin].</param> - /// <param name="progress">The progress.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">package</exception> - Task InstallPackage(PackageVersionInfo package, bool isPlugin, IProgress<double> progress, CancellationToken cancellationToken); + /// <returns><see cref="Task" />.</returns> + Task InstallPackage(InstallationInfo package, CancellationToken cancellationToken = default); /// <summary> - /// Uninstalls a plugin + /// Uninstalls a plugin. /// </summary> /// <param name="plugin">The plugin.</param> - /// <exception cref="ArgumentException"></exception> - void UninstallPlugin(IPlugin plugin); + void UninstallPlugin(LocalPlugin plugin); + + /// <summary> + /// Cancels the installation. + /// </summary> + /// <param name="id">The id of the package that is being installed.</param> + /// <returns>Returns true if the install was cancelled.</returns> + bool CancelInstallation(Guid id); } } diff --git a/MediaBrowser.Common/Updates/InstallationEventArgs.cs b/MediaBrowser.Common/Updates/InstallationEventArgs.cs index 9f215e889..f4f759955 100644 --- a/MediaBrowser.Common/Updates/InstallationEventArgs.cs +++ b/MediaBrowser.Common/Updates/InstallationEventArgs.cs @@ -1,11 +1,23 @@ +#nullable disable + +using System; using MediaBrowser.Model.Updates; namespace MediaBrowser.Common.Updates { - public class InstallationEventArgs + /// <summary> + /// Defines the <see cref="InstallationEventArgs" />. + /// </summary> + public class InstallationEventArgs : EventArgs { + /// <summary> + /// Gets or sets the <see cref="InstallationInfo"/>. + /// </summary> public InstallationInfo InstallationInfo { get; set; } - public PackageVersionInfo PackageVersionInfo { get; set; } + /// <summary> + /// Gets or sets the <see cref="VersionInfo"/>. + /// </summary> + public VersionInfo VersionInfo { get; set; } } } diff --git a/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs b/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs index 43adfb02d..d37146195 100644 --- a/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs +++ b/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs @@ -1,3 +1,6 @@ +#nullable disable +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Common.Updates |
