diff options
| author | Luke <luke.pulverenti@gmail.com> | 2017-08-13 16:21:10 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2017-08-13 16:21:10 -0400 |
| commit | dc578f3742b474bd85d556299a6b2763f4d9acda (patch) | |
| tree | cc4a8d1de140ece77160349e51a64857656ab373 /Emby.Server.Implementations | |
| parent | f6ed934a7e32bf10c3a141773d713bf3b19e784f (diff) | |
| parent | 7f200f057d33e3ef52b1b1b1bf1767577295317e (diff) | |
Merge pull request #2815 from MediaBrowser/beta
Beta
Diffstat (limited to 'Emby.Server.Implementations')
100 files changed, 11853 insertions, 3004 deletions
diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index 567f139fd..702917832 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -18,6 +18,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.Activity { @@ -436,7 +437,7 @@ namespace Emby.Server.Implementations.Activity { Name = string.Format(_localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name), Type = "ScheduledTaskFailed", - Overview = string.Join(Environment.NewLine, vals.ToArray()), + Overview = string.Join(Environment.NewLine, vals.ToArray(vals.Count)), ShortOverview = runningTime, Severity = LogSeverity.Error }); diff --git a/Emby.Server.Implementations/Activity/ActivityRepository.cs b/Emby.Server.Implementations/Activity/ActivityRepository.cs index e9b6f7a40..7720f8f2f 100644 --- a/Emby.Server.Implementations/Activity/ActivityRepository.cs +++ b/Emby.Server.Implementations/Activity/ActivityRepository.cs @@ -10,6 +10,7 @@ using MediaBrowser.Model.Activity; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Querying; using SQLitePCL.pretty; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.Activity { @@ -94,13 +95,13 @@ namespace Emby.Server.Implementations.Activity var whereTextWithoutPaging = whereClauses.Count == 0 ? string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); + " where " + string.Join(" AND ", whereClauses.ToArray(whereClauses.Count)); if (startIndex.HasValue && startIndex.Value > 0) { var pagingWhereText = whereClauses.Count == 0 ? string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); + " where " + string.Join(" AND ", whereClauses.ToArray(whereClauses.Count)); whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM ActivityLogEntries {0} ORDER BY DateCreated DESC LIMIT {1})", pagingWhereText, @@ -109,7 +110,7 @@ namespace Emby.Server.Implementations.Activity var whereText = whereClauses.Count == 0 ? string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); + " where " + string.Join(" AND ", whereClauses.ToArray(whereClauses.Count)); commandText += whereText; @@ -154,7 +155,7 @@ namespace Emby.Server.Implementations.Activity result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); } - result.Items = list.ToArray(); + result.Items = list.ToArray(list.Count); return result; }, ReadTransactionMode); diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs new file mode 100644 index 000000000..7f893d8f7 --- /dev/null +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -0,0 +1,1744 @@ +using Emby.Common.Implementations; +using Emby.Common.Implementations.Archiving; +using Emby.Common.Implementations.IO; +using Emby.Common.Implementations.Reflection; +using Emby.Common.Implementations.ScheduledTasks; +using Emby.Common.Implementations.Serialization; +using Emby.Common.Implementations.TextEncoding; +using Emby.Common.Implementations.Xml; +using Emby.Dlna; +using Emby.Dlna.ConnectionManager; +using Emby.Dlna.ContentDirectory; +using Emby.Dlna.Main; +using Emby.Dlna.MediaReceiverRegistrar; +using Emby.Dlna.Ssdp; +using Emby.Drawing; +using Emby.Photos; +using Emby.Server.Implementations.Activity; +using Emby.Server.Implementations.Channels; +using Emby.Server.Implementations.Collections; +using Emby.Server.Implementations.Configuration; +using Emby.Server.Implementations.Data; +using Emby.Server.Implementations.Devices; +using Emby.Server.Implementations.Dto; +using Emby.Server.Implementations.FFMpeg; +using Emby.Server.Implementations.HttpServer; +using Emby.Server.Implementations.HttpServer.Security; +using Emby.Server.Implementations.IO; +using Emby.Server.Implementations.Library; +using Emby.Server.Implementations.LiveTv; +using Emby.Server.Implementations.Localization; +using Emby.Server.Implementations.MediaEncoder; +using Emby.Server.Implementations.Migrations; +using Emby.Server.Implementations.Notifications; +using Emby.Server.Implementations.Playlists; +using Emby.Server.Implementations.Security; +using Emby.Server.Implementations.Session; +using Emby.Server.Implementations.Social; +using Emby.Server.Implementations.TV; +using Emby.Server.Implementations.Updates; +using MediaBrowser.Api; +using MediaBrowser.Common; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Events; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Common.Progress; +using MediaBrowser.Common.Security; +using MediaBrowser.Common.Updates; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Chapters; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Connect; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Notifications; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Resolvers; +using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.Sorting; +using MediaBrowser.Controller.Subtitles; +using MediaBrowser.Controller.Sync; +using MediaBrowser.Controller.TV; +using MediaBrowser.LocalMetadata.Savers; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Diagnostics; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.News; +using MediaBrowser.Model.Reflection; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.Social; +using MediaBrowser.Model.System; +using MediaBrowser.Model.Text; +using MediaBrowser.Model.Updates; +using MediaBrowser.Model.Xml; +using MediaBrowser.Providers.Chapters; +using MediaBrowser.Providers.Manager; +using MediaBrowser.Providers.Subtitles; +using MediaBrowser.WebDashboard.Api; +using MediaBrowser.XbmcMetadata.Providers; +using OpenSubtitlesHandler; +using ServiceStack; +using SocketHttpListener.Primitives; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Emby.Server.MediaEncoding.Subtitles; +using MediaBrowser.MediaEncoding.BdInfo; +using StringExtensions = MediaBrowser.Controller.Extensions.StringExtensions; + +namespace Emby.Server.Implementations +{ + /// <summary> + /// Class CompositionRoot + /// </summary> + public abstract class ApplicationHost : BaseApplicationHost<ServerApplicationPaths>, IServerApplicationHost, IDependencyContainer + { + /// <summary> + /// Gets the server configuration manager. + /// </summary> + /// <value>The server configuration manager.</value> + public IServerConfigurationManager ServerConfigurationManager + { + get { return (IServerConfigurationManager)ConfigurationManager; } + } + + /// <summary> + /// Gets the configuration manager. + /// </summary> + /// <returns>IConfigurationManager.</returns> + protected override IConfigurationManager GetConfigurationManager() + { + return new ServerConfigurationManager(ApplicationPaths, LogManager, XmlSerializer, FileSystemManager); + } + + /// <summary> + /// Gets or sets the server manager. + /// </summary> + /// <value>The server manager.</value> + private IServerManager ServerManager { get; set; } + /// <summary> + /// Gets or sets the user manager. + /// </summary> + /// <value>The user manager.</value> + public IUserManager UserManager { get; set; } + /// <summary> + /// Gets or sets the library manager. + /// </summary> + /// <value>The library manager.</value> + internal ILibraryManager LibraryManager { get; set; } + /// <summary> + /// Gets or sets the directory watchers. + /// </summary> + /// <value>The directory watchers.</value> + private ILibraryMonitor LibraryMonitor { get; set; } + /// <summary> + /// Gets or sets the provider manager. + /// </summary> + /// <value>The provider manager.</value> + private IProviderManager ProviderManager { get; set; } + /// <summary> + /// Gets or sets the HTTP server. + /// </summary> + /// <value>The HTTP server.</value> + private IHttpServer HttpServer { get; set; } + private IDtoService DtoService { get; set; } + public IImageProcessor ImageProcessor { get; set; } + + /// <summary> + /// Gets or sets the media encoder. + /// </summary> + /// <value>The media encoder.</value> + private IMediaEncoder MediaEncoder { get; set; } + private ISubtitleEncoder SubtitleEncoder { get; set; } + + private IConnectManager ConnectManager { get; set; } + private ISessionManager SessionManager { get; set; } + + private ILiveTvManager LiveTvManager { get; set; } + + public ILocalizationManager LocalizationManager { get; set; } + + private IEncodingManager EncodingManager { get; set; } + private IChannelManager ChannelManager { get; set; } + private ISyncManager SyncManager { get; set; } + + /// <summary> + /// Gets or sets the user data repository. + /// </summary> + /// <value>The user data repository.</value> + private IUserDataManager UserDataManager { get; set; } + private IUserRepository UserRepository { get; set; } + internal IDisplayPreferencesRepository DisplayPreferencesRepository { get; set; } + internal IItemRepository ItemRepository { get; set; } + private INotificationsRepository NotificationsRepository { get; set; } + + private INotificationManager NotificationManager { get; set; } + private ISubtitleManager SubtitleManager { get; set; } + private IChapterManager ChapterManager { get; set; } + private IDeviceManager DeviceManager { get; set; } + + internal IUserViewManager UserViewManager { get; set; } + + private IAuthenticationRepository AuthenticationRepository { get; set; } + private ITVSeriesManager TVSeriesManager { get; set; } + private ICollectionManager CollectionManager { get; set; } + private IMediaSourceManager MediaSourceManager { get; set; } + private IPlaylistManager PlaylistManager { get; set; } + + /// <summary> + /// Gets or sets the installation manager. + /// </summary> + /// <value>The installation manager.</value> + protected IInstallationManager InstallationManager { get; private set; } + /// <summary> + /// Gets the security manager. + /// </summary> + /// <value>The security manager.</value> + protected ISecurityManager SecurityManager { get; private set; } + + /// <summary> + /// Gets or sets the zip client. + /// </summary> + /// <value>The zip client.</value> + protected IZipClient ZipClient { get; private set; } + + protected IAuthService AuthService { get; private set; } + + protected readonly StartupOptions StartupOptions; + private readonly string _releaseAssetFilename; + + internal IPowerManagement PowerManagement { get; private set; } + internal IImageEncoder ImageEncoder { get; private set; } + + private readonly Action<string, string, string> _certificateGenerator; + private readonly Func<string> _defaultUserNameFactory; + + /// <summary> + /// Initializes a new instance of the <see cref="ApplicationHost" /> class. + /// </summary> + public ApplicationHost(ServerApplicationPaths applicationPaths, + ILogManager logManager, + StartupOptions options, + IFileSystem fileSystem, + IPowerManagement powerManagement, + string releaseAssetFilename, + IEnvironmentInfo environmentInfo, + IImageEncoder imageEncoder, + ISystemEvents systemEvents, + IMemoryStreamFactory memoryStreamFactory, + INetworkManager networkManager, + Action<string, string, string> certificateGenerator, + Func<string> defaultUsernameFactory) + : base(applicationPaths, + logManager, + fileSystem, + environmentInfo, + systemEvents, + memoryStreamFactory, + networkManager) + { + StartupOptions = options; + _certificateGenerator = certificateGenerator; + _releaseAssetFilename = releaseAssetFilename; + _defaultUserNameFactory = defaultUsernameFactory; + PowerManagement = powerManagement; + + ImageEncoder = imageEncoder; + + SetBaseExceptionMessage(); + + if (environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows) + { + fileSystem.AddShortcutHandler(new LnkShortcutHandler()); + } + + fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); + } + + private Version _version; + /// <summary> + /// Gets the current application version + /// </summary> + /// <value>The application version.</value> + public override Version ApplicationVersion + { + get + { + return _version ?? (_version = GetAssembly(GetType()).GetName().Version); + } + } + + public virtual bool SupportsRunningAsService + { + get + { + return false; + } + } + + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + public override string Name + { + get + { + return "Emby Server"; + } + } + + public virtual bool IsRunningAsService + { + get + { + return false; + } + } + + private Assembly GetAssembly(Type type) + { + return type.GetTypeInfo().Assembly; + } + + public virtual bool SupportsAutoRunAtStartup + { + get + { + return EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows; + } + } + + private void SetBaseExceptionMessage() + { + var builder = GetBaseExceptionMessage(ApplicationPaths); + + builder.Insert(0, string.Format("Version: {0}{1}", ApplicationVersion, Environment.NewLine)); + builder.Insert(0, "*** Error Report ***" + Environment.NewLine); + + LogManager.ExceptionMessagePrefix = builder.ToString(); + } + + /// <summary> + /// Runs the startup tasks. + /// </summary> + public override async Task RunStartupTasks() + { + await PerformPreInitMigrations().ConfigureAwait(false); + + await base.RunStartupTasks().ConfigureAwait(false); + + await MediaEncoder.Init().ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath)) + { + if (ServerConfigurationManager.Configuration.IsStartupWizardCompleted) + { + ServerConfigurationManager.Configuration.IsStartupWizardCompleted = false; + ServerConfigurationManager.SaveConfiguration(); + } + } + + Logger.Info("ServerId: {0}", SystemId); + Logger.Info("Core startup complete"); + HttpServer.GlobalResponse = null; + + PerformPostInitMigrations(); + Logger.Info("Post-init migrations complete"); + + foreach (var entryPoint in GetExports<IServerEntryPoint>().ToList()) + { + var name = entryPoint.GetType().FullName; + Logger.Info("Starting entry point {0}", name); + var now = DateTime.UtcNow; + try + { + entryPoint.Run(); + } + catch (Exception ex) + { + Logger.ErrorException("Error in {0}", ex, name); + } + Logger.Info("Entry point completed: {0}. Duration: {1} seconds", name, (DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture), "ImageInfos"); + } + Logger.Info("All entry points have started"); + + LogManager.RemoveConsoleOutput(); + } + + protected override IJsonSerializer CreateJsonSerializer() + { + try + { + // https://github.com/ServiceStack/ServiceStack/blob/master/tests/ServiceStack.WebHost.IntegrationTests/Web.config#L4 + Licensing.RegisterLicense("1001-e1JlZjoxMDAxLE5hbWU6VGVzdCBCdXNpbmVzcyxUeXBlOkJ1c2luZXNzLEhhc2g6UHVNTVRPclhvT2ZIbjQ5MG5LZE1mUTd5RUMzQnBucTFEbTE3TDczVEF4QUNMT1FhNXJMOWkzVjFGL2ZkVTE3Q2pDNENqTkQyUktRWmhvUVBhYTBiekJGUUZ3ZE5aZHFDYm9hL3lydGlwUHI5K1JsaTBYbzNsUC85cjVJNHE5QVhldDN6QkE4aTlvdldrdTgyTk1relY2eis2dFFqTThYN2lmc0JveHgycFdjPSxFeHBpcnk6MjAxMy0wMS0wMX0="); + } + catch + { + // Failing under mono + } + + var result = new JsonSerializer(FileSystemManager, LogManager.GetLogger("JsonSerializer")); + + ServiceStack.Text.JsConfig<LiveTvProgram>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<LiveTvChannel>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<LiveTvVideoRecording>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<LiveTvAudioRecording>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<Series>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<Audio>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<MusicAlbum>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<MusicArtist>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<MusicGenre>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<MusicVideo>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<Movie>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<Playlist>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<AudioPodcast>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<AudioBook>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<Trailer>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<BoxSet>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<Episode>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<Season>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<Book>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<CollectionFolder>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<Folder>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<Game>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<GameGenre>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<GameSystem>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<Genre>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<Person>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<Photo>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<PhotoAlbum>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<Studio>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<UserRootFolder>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<UserView>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<Video>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<Year>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<Channel>.ExcludePropertyNames = new[] { "ProviderIds" }; + ServiceStack.Text.JsConfig<AggregateFolder>.ExcludePropertyNames = new[] { "ProviderIds" }; + + return result; + } + + public override Task Init(IProgress<double> progress) + { + HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber; + HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber; + + // Safeguard against invalid configuration + if (HttpPort == HttpsPort) + { + HttpPort = ServerConfiguration.DefaultHttpPort; + HttpsPort = ServerConfiguration.DefaultHttpsPort; + } + + return base.Init(progress); + } + + private async Task PerformPreInitMigrations() + { + var migrations = new List<IVersionMigration> + { + }; + + foreach (var task in migrations) + { + try + { + await task.Run().ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.ErrorException("Error running migration", ex); + } + } + } + + private void PerformPostInitMigrations() + { + var migrations = new List<IVersionMigration> + { + }; + + foreach (var task in migrations) + { + try + { + task.Run(); + } + catch (Exception ex) + { + Logger.ErrorException("Error running migration", ex); + } + } + } + + protected abstract IConnectManager CreateConnectManager(); + protected abstract ISyncManager CreateSyncManager(); + + /// <summary> + /// Registers resources that classes will depend on + /// </summary> + protected override async Task RegisterResources(IProgress<double> progress) + { + await base.RegisterResources(progress).ConfigureAwait(false); + + RegisterSingleInstance(PowerManagement); + + SecurityManager = new PluginSecurityManager(this, HttpClient, JsonSerializer, ApplicationPaths, LogManager, FileSystemManager, CryptographyProvider); + RegisterSingleInstance(SecurityManager); + + InstallationManager = new InstallationManager(LogManager.GetLogger("InstallationManager"), this, ApplicationPaths, HttpClient, JsonSerializer, SecurityManager, ConfigurationManager, FileSystemManager, CryptographyProvider); + RegisterSingleInstance(InstallationManager); + + ZipClient = new ZipClient(FileSystemManager); + RegisterSingleInstance(ZipClient); + + RegisterSingleInstance<IHttpResultFactory>(new HttpResultFactory(LogManager, FileSystemManager, JsonSerializer, MemoryStreamFactory)); + + RegisterSingleInstance<IServerApplicationHost>(this); + RegisterSingleInstance<IServerApplicationPaths>(ApplicationPaths); + + RegisterSingleInstance(ServerConfigurationManager); + + IAssemblyInfo assemblyInfo = new AssemblyInfo(); + RegisterSingleInstance<IAssemblyInfo>(assemblyInfo); + + LocalizationManager = new LocalizationManager(ServerConfigurationManager, FileSystemManager, JsonSerializer, LogManager.GetLogger("LocalizationManager"), assemblyInfo, new TextLocalizer()); + StringExtensions.LocalizationManager = LocalizationManager; + RegisterSingleInstance(LocalizationManager); + + ITextEncoding textEncoding = new TextEncoding(FileSystemManager, LogManager.GetLogger("TextEncoding"), JsonSerializer); + RegisterSingleInstance(textEncoding); + Utilities.EncodingHelper = textEncoding; + RegisterSingleInstance<IBlurayExaminer>(() => new BdInfoExaminer(FileSystemManager, textEncoding)); + + RegisterSingleInstance<IXmlReaderSettingsFactory>(new XmlReaderSettingsFactory()); + + UserDataManager = new UserDataManager(LogManager, ServerConfigurationManager); + RegisterSingleInstance(UserDataManager); + + UserRepository = GetUserRepository(); + // This is only needed for disposal purposes. If removing this, make sure to have the manager handle disposing it + RegisterSingleInstance(UserRepository); + + var displayPreferencesRepo = new SqliteDisplayPreferencesRepository(LogManager.GetLogger("SqliteDisplayPreferencesRepository"), JsonSerializer, ApplicationPaths, MemoryStreamFactory); + DisplayPreferencesRepository = displayPreferencesRepo; + RegisterSingleInstance(DisplayPreferencesRepository); + + var itemRepo = new SqliteItemRepository(ServerConfigurationManager, JsonSerializer, LogManager.GetLogger("SqliteItemRepository"), MemoryStreamFactory, assemblyInfo, FileSystemManager, EnvironmentInfo, TimerFactory); + ItemRepository = itemRepo; + RegisterSingleInstance(ItemRepository); + + AuthenticationRepository = GetAuthenticationRepository(); + RegisterSingleInstance(AuthenticationRepository); + + UserManager = new UserManager(LogManager.GetLogger("UserManager"), ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, () => ConnectManager, this, JsonSerializer, FileSystemManager, CryptographyProvider, _defaultUserNameFactory()); + RegisterSingleInstance(UserManager); + + LibraryManager = new LibraryManager(Logger, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager); + RegisterSingleInstance(LibraryManager); + + var musicManager = new MusicManager(LibraryManager); + RegisterSingleInstance<IMusicManager>(new MusicManager(LibraryManager)); + + LibraryMonitor = new LibraryMonitor(LogManager, TaskManager, LibraryManager, ServerConfigurationManager, FileSystemManager, TimerFactory, SystemEvents, EnvironmentInfo); + RegisterSingleInstance(LibraryMonitor); + + ProviderManager = new ProviderManager(HttpClient, ServerConfigurationManager, LibraryMonitor, LogManager, FileSystemManager, ApplicationPaths, () => LibraryManager, JsonSerializer, MemoryStreamFactory); + RegisterSingleInstance(ProviderManager); + + RegisterSingleInstance<ISearchEngine>(() => new SearchEngine(LogManager, LibraryManager, UserManager)); + + CertificateInfo = GetCertificateInfo(true); + Certificate = GetCertificate(CertificateInfo); + + HttpServer = HttpServerFactory.CreateServer(this, LogManager, ServerConfigurationManager, NetworkManager, MemoryStreamFactory, "Emby", "web/index.html", textEncoding, SocketFactory, CryptographyProvider, JsonSerializer, XmlSerializer, EnvironmentInfo, Certificate, FileSystemManager, SupportsDualModeSockets); + HttpServer.GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); + RegisterSingleInstance(HttpServer, false); + progress.Report(10); + + ServerManager = new ServerManager.ServerManager(this, JsonSerializer, LogManager.GetLogger("ServerManager"), ServerConfigurationManager, MemoryStreamFactory, textEncoding); + RegisterSingleInstance(ServerManager); + + var innerProgress = new ActionableProgress<double>(); + innerProgress.RegisterAction(p => progress.Report((.75 * p) + 15)); + + ImageProcessor = GetImageProcessor(); + RegisterSingleInstance(ImageProcessor); + + TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager); + RegisterSingleInstance(TVSeriesManager); + + SyncManager = CreateSyncManager(); + RegisterSingleInstance(SyncManager); + + DtoService = new DtoService(LogManager.GetLogger("DtoService"), LibraryManager, UserDataManager, ItemRepository, ImageProcessor, ServerConfigurationManager, FileSystemManager, ProviderManager, () => ChannelManager, SyncManager, this, () => DeviceManager, () => MediaSourceManager, () => LiveTvManager); + RegisterSingleInstance(DtoService); + + var encryptionManager = new EncryptionManager(); + RegisterSingleInstance<IEncryptionManager>(encryptionManager); + + ConnectManager = CreateConnectManager(); + RegisterSingleInstance(ConnectManager); + + DeviceManager = new DeviceManager(new DeviceRepository(ApplicationPaths, JsonSerializer, LogManager.GetLogger("DeviceManager"), FileSystemManager), UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager, LogManager.GetLogger("DeviceManager"), NetworkManager); + RegisterSingleInstance(DeviceManager); + + var newsService = new Emby.Server.Implementations.News.NewsService(ApplicationPaths, JsonSerializer); + RegisterSingleInstance<INewsService>(newsService); + + progress.Report(15); + + ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LogManager.GetLogger("ChannelManager"), ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, LocalizationManager, HttpClient, ProviderManager); + RegisterSingleInstance(ChannelManager); + + MediaSourceManager = new MediaSourceManager(ItemRepository, UserManager, LibraryManager, LogManager.GetLogger("MediaSourceManager"), JsonSerializer, FileSystemManager, UserDataManager, TimerFactory); + RegisterSingleInstance(MediaSourceManager); + + SessionManager = new SessionManager(UserDataManager, LogManager.GetLogger("SessionManager"), LibraryManager, UserManager, musicManager, DtoService, ImageProcessor, JsonSerializer, this, HttpClient, AuthenticationRepository, DeviceManager, MediaSourceManager, TimerFactory); + RegisterSingleInstance(SessionManager); + + var dlnaManager = new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LogManager.GetLogger("Dlna"), JsonSerializer, this, assemblyInfo); + RegisterSingleInstance<IDlnaManager>(dlnaManager); + + var connectionManager = new ConnectionManager(dlnaManager, ServerConfigurationManager, LogManager.GetLogger("UpnpConnectionManager"), HttpClient, new XmlReaderSettingsFactory()); + RegisterSingleInstance<IConnectionManager>(connectionManager); + + CollectionManager = new CollectionManager(LibraryManager, FileSystemManager, LibraryMonitor, LogManager.GetLogger("CollectionManager"), ProviderManager); + RegisterSingleInstance(CollectionManager); + + PlaylistManager = new PlaylistManager(LibraryManager, FileSystemManager, LibraryMonitor, LogManager.GetLogger("PlaylistManager"), UserManager, ProviderManager); + RegisterSingleInstance<IPlaylistManager>(PlaylistManager); + + LiveTvManager = new LiveTvManager(this, ServerConfigurationManager, Logger, ItemRepository, ImageProcessor, UserDataManager, DtoService, UserManager, LibraryManager, TaskManager, LocalizationManager, JsonSerializer, ProviderManager, FileSystemManager, SecurityManager); + RegisterSingleInstance(LiveTvManager); + + UserViewManager = new UserViewManager(LibraryManager, LocalizationManager, UserManager, ChannelManager, LiveTvManager, ServerConfigurationManager); + RegisterSingleInstance(UserViewManager); + + var contentDirectory = new ContentDirectory(dlnaManager, UserDataManager, ImageProcessor, LibraryManager, ServerConfigurationManager, UserManager, LogManager.GetLogger("UpnpContentDirectory"), HttpClient, LocalizationManager, ChannelManager, MediaSourceManager, UserViewManager, () => MediaEncoder, new XmlReaderSettingsFactory(), TVSeriesManager); + RegisterSingleInstance<IContentDirectory>(contentDirectory); + + var mediaRegistrar = new MediaReceiverRegistrar(LogManager.GetLogger("MediaReceiverRegistrar"), HttpClient, ServerConfigurationManager, new XmlReaderSettingsFactory()); + RegisterSingleInstance<IMediaReceiverRegistrar>(mediaRegistrar); + + NotificationManager = new NotificationManager(LogManager, UserManager, ServerConfigurationManager); + RegisterSingleInstance(NotificationManager); + + SubtitleManager = new SubtitleManager(LogManager.GetLogger("SubtitleManager"), FileSystemManager, LibraryMonitor, LibraryManager, MediaSourceManager); + RegisterSingleInstance(SubtitleManager); + + RegisterSingleInstance<IDeviceDiscovery>(new DeviceDiscovery(LogManager.GetLogger("IDeviceDiscovery"), ServerConfigurationManager, SocketFactory, TimerFactory)); + + ChapterManager = new ChapterManager(LibraryManager, LogManager.GetLogger("ChapterManager"), ServerConfigurationManager, ItemRepository); + RegisterSingleInstance(ChapterManager); + + await RegisterMediaEncoder(innerProgress).ConfigureAwait(false); + progress.Report(90); + + EncodingManager = new EncodingManager(FileSystemManager, Logger, MediaEncoder, ChapterManager, LibraryManager); + RegisterSingleInstance(EncodingManager); + + var sharingRepo = new SharingRepository(LogManager.GetLogger("SharingRepository"), ApplicationPaths); + sharingRepo.Initialize(); + // This is only needed for disposal purposes. If removing this, make sure to have the manager handle disposing it + RegisterSingleInstance<ISharingRepository>(sharingRepo); + RegisterSingleInstance<ISharingManager>(new SharingManager(sharingRepo, ServerConfigurationManager, LibraryManager, this)); + + var activityLogRepo = GetActivityLogRepository(); + RegisterSingleInstance(activityLogRepo); + RegisterSingleInstance<IActivityManager>(new ActivityManager(LogManager.GetLogger("ActivityManager"), activityLogRepo, UserManager)); + + var authContext = new AuthorizationContext(AuthenticationRepository, ConnectManager); + RegisterSingleInstance<IAuthorizationContext>(authContext); + RegisterSingleInstance<ISessionContext>(new SessionContext(UserManager, authContext, SessionManager)); + + AuthService = new AuthService(UserManager, authContext, ServerConfigurationManager, ConnectManager, SessionManager, DeviceManager); + RegisterSingleInstance<IAuthService>(AuthService); + + SubtitleEncoder = new SubtitleEncoder(LibraryManager, LogManager.GetLogger("SubtitleEncoder"), ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer, HttpClient, MediaSourceManager, MemoryStreamFactory, ProcessFactory, textEncoding); + RegisterSingleInstance(SubtitleEncoder); + + displayPreferencesRepo.Initialize(); + + var userDataRepo = new SqliteUserDataRepository(LogManager.GetLogger("SqliteUserDataRepository"), ApplicationPaths, FileSystemManager); + + ((UserDataManager)UserDataManager).Repository = userDataRepo; + itemRepo.Initialize(userDataRepo); + ((LibraryManager)LibraryManager).ItemRepository = ItemRepository; + ConfigureNotificationsRepository(); + progress.Report(100); + + SetStaticProperties(); + + await ((UserManager)UserManager).Initialize().ConfigureAwait(false); + } + + protected virtual bool SupportsDualModeSockets + { + get + { + return true; + } + } + + private ICertificate GetCertificate(CertificateInfo info) + { + var certificateLocation = info == null ? null : info.Path; + + if (string.IsNullOrWhiteSpace(certificateLocation)) + { + return null; + } + + try + { + if (!FileSystemManager.FileExists(certificateLocation)) + { + return null; + } + + // Don't use an empty string password + var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password; + + X509Certificate2 localCert = new X509Certificate2(certificateLocation, password); + //localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA; + if (!localCert.HasPrivateKey) + { + //throw new FileNotFoundException("Secure requested, no private key included", certificateLocation); + return null; + } + + return new Certificate(localCert); + } + catch (Exception ex) + { + Logger.ErrorException("Error loading cert from {0}", ex, certificateLocation); + return null; + } + } + + private IImageProcessor GetImageProcessor() + { + return new ImageProcessor(LogManager.GetLogger("ImageProcessor"), ServerConfigurationManager.ApplicationPaths, FileSystemManager, JsonSerializer, ImageEncoder, () => LibraryManager, TimerFactory); + } + + protected virtual FFMpegInstallInfo GetFfmpegInstallInfo() + { + var info = new FFMpegInstallInfo(); + + // Windows builds: http://ffmpeg.zeranoe.com/builds/ + // Linux builds: http://johnvansickle.com/ffmpeg/ + // OS X builds: http://ffmpegmac.net/ + // OS X x64: http://www.evermeet.cx/ffmpeg/ + + if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Linux) + { + info.FFMpegFilename = "ffmpeg"; + info.FFProbeFilename = "ffprobe"; + info.ArchiveType = "7z"; + info.Version = "20170308"; + info.DownloadUrls = GetLinuxDownloadUrls(); + } + else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows) + { + info.FFMpegFilename = "ffmpeg.exe"; + info.FFProbeFilename = "ffprobe.exe"; + info.Version = "20170308"; + info.ArchiveType = "7z"; + info.DownloadUrls = GetWindowsDownloadUrls(); + } + else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX) + { + info.FFMpegFilename = "ffmpeg"; + info.FFProbeFilename = "ffprobe"; + info.ArchiveType = "7z"; + info.Version = "20170308"; + info.DownloadUrls = GetMacDownloadUrls(); + } + else + { + // No version available - user requirement + info.DownloadUrls = new string[] { }; + } + + return info; + } + + private string[] GetMacDownloadUrls() + { + switch (EnvironmentInfo.SystemArchitecture) + { + case Architecture.X64: + return new[] + { + "https://embydata.com/downloads/ffmpeg/osx/ffmpeg-x64-20170308.7z" + }; + } + + return new string[] { }; + } + + private string[] GetWindowsDownloadUrls() + { + switch (EnvironmentInfo.SystemArchitecture) + { + case Architecture.X64: + return new[] + { + "https://embydata.com/downloads/ffmpeg/windows/ffmpeg-20170308-win64.7z" + }; + case Architecture.X86: + return new[] + { + "https://embydata.com/downloads/ffmpeg/windows/ffmpeg-20170308-win32.7z" + }; + } + + return new string[] { }; + } + + private string[] GetLinuxDownloadUrls() + { + switch (EnvironmentInfo.SystemArchitecture) + { + case Architecture.X64: + return new[] + { + "https://embydata.com/downloads/ffmpeg/linux/ffmpeg-git-20170301-64bit-static.7z" + }; + case Architecture.X86: + return new[] + { + "https://embydata.com/downloads/ffmpeg/linux/ffmpeg-git-20170301-32bit-static.7z" + }; + } + + return new string[] { }; + } + + /// <summary> + /// Registers the media encoder. + /// </summary> + /// <returns>Task.</returns> + private async Task RegisterMediaEncoder(IProgress<double> progress) + { + string encoderPath = null; + string probePath = null; + + var info = await new FFMpegLoader(Logger, ApplicationPaths, HttpClient, ZipClient, FileSystemManager, GetFfmpegInstallInfo()) + .GetFFMpegInfo(StartupOptions, progress).ConfigureAwait(false); + + encoderPath = info.EncoderPath; + probePath = info.ProbePath; + var hasExternalEncoder = string.Equals(info.Version, "external", StringComparison.OrdinalIgnoreCase); + + var mediaEncoder = new MediaEncoding.Encoder.MediaEncoder(LogManager.GetLogger("MediaEncoder"), + JsonSerializer, + encoderPath, + probePath, + hasExternalEncoder, + ServerConfigurationManager, + FileSystemManager, + LiveTvManager, + IsoManager, + LibraryManager, + ChannelManager, + SessionManager, + () => SubtitleEncoder, + () => MediaSourceManager, + HttpClient, + ZipClient, + MemoryStreamFactory, + ProcessFactory, + (Environment.ProcessorCount > 2 ? 14000 : 40000), + EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows, + EnvironmentInfo); + + MediaEncoder = mediaEncoder; + RegisterSingleInstance(MediaEncoder); + } + + /// <summary> + /// Gets the user repository. + /// </summary> + /// <returns>Task{IUserRepository}.</returns> + private IUserRepository GetUserRepository() + { + var repo = new SqliteUserRepository(LogManager.GetLogger("SqliteUserRepository"), ApplicationPaths, JsonSerializer, MemoryStreamFactory); + + repo.Initialize(); + + return repo; + } + + private IAuthenticationRepository GetAuthenticationRepository() + { + var repo = new AuthenticationRepository(LogManager.GetLogger("AuthenticationRepository"), ServerConfigurationManager.ApplicationPaths); + + repo.Initialize(); + + return repo; + } + + private IActivityRepository GetActivityLogRepository() + { + var repo = new ActivityRepository(LogManager.GetLogger("ActivityRepository"), ServerConfigurationManager.ApplicationPaths); + + repo.Initialize(); + + return repo; + } + + /// <summary> + /// Configures the repositories. + /// </summary> + private void ConfigureNotificationsRepository() + { + var repo = new SqliteNotificationsRepository(LogManager.GetLogger("SqliteNotificationsRepository"), ServerConfigurationManager.ApplicationPaths, FileSystemManager); + + repo.Initialize(); + + NotificationsRepository = repo; + + RegisterSingleInstance(NotificationsRepository); + } + + /// <summary> + /// Dirty hacks + /// </summary> + private void SetStaticProperties() + { + // For now there's no real way to inject these properly + BaseItem.Logger = LogManager.GetLogger("BaseItem"); + BaseItem.ConfigurationManager = ServerConfigurationManager; + BaseItem.LibraryManager = LibraryManager; + BaseItem.ProviderManager = ProviderManager; + BaseItem.LocalizationManager = LocalizationManager; + BaseItem.ItemRepository = ItemRepository; + User.XmlSerializer = XmlSerializer; + User.UserManager = UserManager; + Folder.UserManager = UserManager; + BaseItem.FileSystem = FileSystemManager; + BaseItem.UserDataManager = UserDataManager; + BaseItem.ChannelManager = ChannelManager; + BaseItem.LiveTvManager = LiveTvManager; + Folder.UserViewManager = UserViewManager; + UserView.TVSeriesManager = TVSeriesManager; + UserView.PlaylistManager = PlaylistManager; + BaseItem.CollectionManager = CollectionManager; + BaseItem.MediaSourceManager = MediaSourceManager; + CollectionFolder.XmlSerializer = XmlSerializer; + Utilities.CryptographyProvider = CryptographyProvider; + AuthenticatedAttribute.AuthService = AuthService; + } + + /// <summary> + /// Finds the parts. + /// </summary> + protected override void FindParts() + { + if (!ServerConfigurationManager.Configuration.IsPortAuthorized) + { + RegisterServerWithAdministratorAccess(); + ServerConfigurationManager.Configuration.IsPortAuthorized = true; + ConfigurationManager.SaveConfiguration(); + } + + RegisterModules(); + + base.FindParts(); + + HttpServer.Init(GetExports<IService>(false)); + + ServerManager.AddWebSocketListeners(GetExports<IWebSocketListener>(false)); + + StartServer(); + + LibraryManager.AddParts(GetExports<IResolverIgnoreRule>(), + GetExports<IVirtualFolderCreator>(), + GetExports<IItemResolver>(), + GetExports<IIntroProvider>(), + GetExports<IBaseItemComparer>(), + GetExports<ILibraryPostScanTask>()); + + ProviderManager.AddParts(GetExports<IImageProvider>(), + GetExports<IMetadataService>(), + GetExports<IMetadataProvider>(), + GetExports<IMetadataSaver>(), + GetExports<IExternalId>()); + + ImageProcessor.AddParts(GetExports<IImageEnhancer>()); + + LiveTvManager.AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>()); + + SubtitleManager.AddParts(GetExports<ISubtitleProvider>()); + + SessionManager.AddParts(GetExports<ISessionControllerFactory>()); + + ChannelManager.AddParts(GetExports<IChannel>()); + + MediaSourceManager.AddParts(GetExports<IMediaSourceProvider>()); + + NotificationManager.AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>()); + SyncManager.AddParts(GetExports<ISyncProvider>()); + } + + private CertificateInfo CertificateInfo { get; set; } + private ICertificate Certificate { get; set; } + + private IEnumerable<string> GetUrlPrefixes() + { + var hosts = new List<string>(); + + hosts.Add("+"); + + return hosts.SelectMany(i => + { + var prefixes = new List<string> + { + "http://"+i+":" + HttpPort + "/" + }; + + if (CertificateInfo != null) + { + prefixes.Add("https://" + i + ":" + HttpsPort + "/"); + } + + return prefixes; + }); + } + + /// <summary> + /// Starts the server. + /// </summary> + private void StartServer() + { + try + { + ServerManager.Start(GetUrlPrefixes()); + return; + } + catch (Exception ex) + { + Logger.ErrorException("Error starting http server", ex); + + if (HttpPort == ServerConfiguration.DefaultHttpPort) + { + throw; + } + } + + HttpPort = ServerConfiguration.DefaultHttpPort; + + try + { + ServerManager.Start(GetUrlPrefixes()); + } + catch (Exception ex) + { + Logger.ErrorException("Error starting http server", ex); + + throw; + } + } + + private CertificateInfo GetCertificateInfo(bool generateCertificate) + { + if (!string.IsNullOrWhiteSpace(ServerConfigurationManager.Configuration.CertificatePath)) + { + // Custom cert + return new CertificateInfo + { + Path = ServerConfigurationManager.Configuration.CertificatePath, + Password = ServerConfigurationManager.Configuration.CertificatePassword + }; + } + + // Generate self-signed cert + var certHost = GetHostnameFromExternalDns(ServerConfigurationManager.Configuration.WanDdns); + var certPath = Path.Combine(ServerConfigurationManager.ApplicationPaths.ProgramDataPath, "ssl", "cert_" + (certHost + "2").GetMD5().ToString("N") + ".pfx"); + var password = "embycert"; + + if (generateCertificate) + { + if (!FileSystemManager.FileExists(certPath)) + { + FileSystemManager.CreateDirectory(FileSystemManager.GetDirectoryName(certPath)); + + try + { + _certificateGenerator(certPath, certHost, password); + } + catch (Exception ex) + { + Logger.ErrorException("Error creating ssl cert", ex); + return null; + } + } + } + + return new CertificateInfo + { + Path = certPath, + Password = password + }; + } + + /// <summary> + /// Called when [configuration updated]. + /// </summary> + /// <param name="sender">The sender.</param> + /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param> + protected override void OnConfigurationUpdated(object sender, EventArgs e) + { + base.OnConfigurationUpdated(sender, e); + + var requiresRestart = false; + + // Don't do anything if these haven't been set yet + if (HttpPort != 0 && HttpsPort != 0) + { + // Need to restart if ports have changed + if (ServerConfigurationManager.Configuration.HttpServerPortNumber != HttpPort || + ServerConfigurationManager.Configuration.HttpsPortNumber != HttpsPort) + { + if (ServerConfigurationManager.Configuration.IsPortAuthorized) + { + ServerConfigurationManager.Configuration.IsPortAuthorized = false; + ServerConfigurationManager.SaveConfiguration(); + + requiresRestart = true; + } + } + } + + if (!HttpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase)) + { + requiresRestart = true; + } + + var currentCertPath = CertificateInfo == null ? null : CertificateInfo.Path; + var newCertInfo = GetCertificateInfo(false); + var newCertPath = newCertInfo == null ? null : newCertInfo.Path; + + if (!string.Equals(currentCertPath, newCertPath, StringComparison.OrdinalIgnoreCase)) + { + requiresRestart = true; + } + + if (requiresRestart) + { + Logger.Info("App needs to be restarted due to configuration change."); + + NotifyPendingRestart(); + } + } + + /// <summary> + /// Restarts this instance. + /// </summary> + public override async Task Restart() + { + if (!CanSelfRestart) + { + throw new PlatformNotSupportedException("The server is unable to self-restart. Please restart manually."); + } + + try + { + await SessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.ErrorException("Error sending server restart notification", ex); + } + + Logger.Info("Calling RestartInternal"); + + RestartInternal(); + } + + protected abstract void RestartInternal(); + + /// <summary> + /// Gets the composable part assemblies. + /// </summary> + /// <returns>IEnumerable{Assembly}.</returns> + protected override IEnumerable<Assembly> GetComposablePartAssemblies() + { + var list = GetPluginAssemblies() + .ToList(); + + // Gets all plugin assemblies by first reading all bytes of the .dll and calling Assembly.Load against that + // This will prevent the .dll file from getting locked, and allow us to replace it when needed + + // Include composable parts in the Api assembly + list.Add(GetAssembly(typeof(ApiEntryPoint))); + + // Include composable parts in the Dashboard assembly + list.Add(GetAssembly(typeof(DashboardService))); + + // Include composable parts in the Model assembly + list.Add(GetAssembly(typeof(SystemInfo))); + + // Include composable parts in the Common assembly + list.Add(GetAssembly(typeof(IApplicationHost))); + + // Include composable parts in the Controller assembly + list.Add(GetAssembly(typeof(IServerApplicationHost))); + + // Include composable parts in the Providers assembly + list.Add(GetAssembly(typeof(ProviderUtils))); + + // Include composable parts in the Photos assembly + list.Add(GetAssembly(typeof(PhotoProvider))); + + // Common implementations + list.Add(GetAssembly(typeof(TaskManager))); + + // Emby.Server implementations + list.Add(GetAssembly(typeof(InstallationManager))); + + // MediaEncoding + list.Add(GetAssembly(typeof(MediaEncoding.Encoder.MediaEncoder))); + + // Dlna + list.Add(GetAssembly(typeof(DlnaEntryPoint))); + + // Local metadata + list.Add(GetAssembly(typeof(BoxSetXmlSaver))); + + // Xbmc + list.Add(GetAssembly(typeof(ArtistNfoProvider))); + + list.AddRange(GetAssembliesWithPartsInternal()); + + return list.ToList(); + } + + protected abstract List<Assembly> GetAssembliesWithPartsInternal(); + + /// <summary> + /// Gets the plugin assemblies. + /// </summary> + /// <returns>IEnumerable{Assembly}.</returns> + private IEnumerable<Assembly> GetPluginAssemblies() + { + try + { + return Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.TopDirectoryOnly) + .Where(EnablePlugin) + .Select(LoadAssembly) + .Where(a => a != null) + .ToList(); + } + catch (DirectoryNotFoundException) + { + return new List<Assembly>(); + } + } + + private bool EnablePlugin(string path) + { + var filename = Path.GetFileName(path); + + var exclude = new[] + { + "mbplus.dll", + "mbintros.dll", + "embytv.dll" + }; + + return !exclude.Contains(filename ?? string.Empty, StringComparer.OrdinalIgnoreCase); + } + + /// <summary> + /// Gets the system status. + /// </summary> + /// <returns>SystemInfo.</returns> + public async Task<SystemInfo> GetSystemInfo() + { + var localAddress = await GetLocalApiUrl().ConfigureAwait(false); + + return new SystemInfo + { + HasPendingRestart = HasPendingRestart, + Version = ApplicationVersion.ToString(), + WebSocketPortNumber = HttpPort, + FailedPluginAssemblies = FailedAssemblies.ToList(), + InProgressInstallations = InstallationManager.CurrentInstallations.Select(i => i.Item1).ToList(), + CompletedInstallations = InstallationManager.CompletedInstallations.ToList(), + Id = SystemId, + ProgramDataPath = ApplicationPaths.ProgramDataPath, + LogPath = ApplicationPaths.LogDirectoryPath, + ItemsByNamePath = ApplicationPaths.ItemsByNamePath, + InternalMetadataPath = ApplicationPaths.InternalMetadataPath, + CachePath = ApplicationPaths.CachePath, + MacAddress = GetMacAddress(), + HttpServerPortNumber = HttpPort, + SupportsHttps = SupportsHttps, + HttpsPortNumber = HttpsPort, + OperatingSystem = EnvironmentInfo.OperatingSystem.ToString(), + OperatingSystemDisplayName = OperatingSystemDisplayName, + CanSelfRestart = CanSelfRestart, + CanSelfUpdate = CanSelfUpdate, + WanAddress = ConnectManager.WanApiAddress, + HasUpdateAvailable = HasUpdateAvailable, + SupportsAutoRunAtStartup = SupportsAutoRunAtStartup, + TranscodingTempPath = ApplicationPaths.TranscodingTempPath, + SupportsRunningAsService = SupportsRunningAsService, + ServerName = FriendlyName, + LocalAddress = localAddress, + SupportsLibraryMonitor = true, + EncoderLocationType = MediaEncoder.EncoderLocationType, + SystemArchitecture = EnvironmentInfo.SystemArchitecture, + SystemUpdateLevel = SystemUpdateLevel, + PackageName = StartupOptions.GetOption("-package") + }; + } + + public bool EnableHttps + { + get + { + return SupportsHttps && ServerConfigurationManager.Configuration.EnableHttps; + } + } + + public bool SupportsHttps + { + get { return Certificate != null; } + } + + public async Task<string> GetLocalApiUrl() + { + try + { + // Return the first matched address, if found, or the first known local address + var address = (await GetLocalIpAddresses().ConfigureAwait(false)).FirstOrDefault(i => !i.Equals(IpAddressInfo.Loopback) && !i.Equals(IpAddressInfo.IPv6Loopback)); + + if (address != null) + { + return GetLocalApiUrl(address); + } + + return null; + } + catch (Exception ex) + { + Logger.ErrorException("Error getting local Ip address information", ex); + } + + return null; + } + + public string GetLocalApiUrl(IpAddressInfo ipAddress) + { + if (ipAddress.AddressFamily == IpAddressFamily.InterNetworkV6) + { + return GetLocalApiUrl("[" + ipAddress.Address + "]"); + } + + return GetLocalApiUrl(ipAddress.Address); + } + + public string GetLocalApiUrl(string host) + { + return string.Format("http://{0}:{1}", + host, + HttpPort.ToString(CultureInfo.InvariantCulture)); + } + + public async Task<List<IpAddressInfo>> GetLocalIpAddresses() + { + var addresses = ServerConfigurationManager + .Configuration + .LocalNetworkAddresses + .Select(NormalizeConfiguredLocalAddress) + .Where(i => i != null) + .ToList(); + + if (addresses.Count == 0) + { + addresses.AddRange(NetworkManager.GetLocalIpAddresses()); + + var list = new List<IpAddressInfo>(); + + foreach (var address in addresses) + { + var valid = await IsIpAddressValidAsync(address).ConfigureAwait(false); + if (valid) + { + list.Add(address); + } + } + + addresses = list; + } + + return addresses; + } + + private IpAddressInfo NormalizeConfiguredLocalAddress(string address) + { + var index = address.Trim('/').IndexOf('/'); + + if (index != -1) + { + address = address.Substring(index + 1); + } + + IpAddressInfo result; + if (NetworkManager.TryParseIpAddress(address.Trim('/'), out result)) + { + return result; + } + return null; + } + + private readonly ConcurrentDictionary<string, bool> _validAddressResults = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase); + private DateTime _lastAddressCacheClear; + private async Task<bool> IsIpAddressValidAsync(IpAddressInfo address) + { + if (address.Equals(IpAddressInfo.Loopback) || + address.Equals(IpAddressInfo.IPv6Loopback)) + { + return true; + } + + var apiUrl = GetLocalApiUrl(address); + apiUrl += "/system/ping"; + + if ((DateTime.UtcNow - _lastAddressCacheClear).TotalMinutes >= 15) + { + _lastAddressCacheClear = DateTime.UtcNow; + _validAddressResults.Clear(); + } + + bool cachedResult; + if (_validAddressResults.TryGetValue(apiUrl, out cachedResult)) + { + return cachedResult; + } + + try + { + using (var response = await HttpClient.SendAsync(new HttpRequestOptions + { + Url = apiUrl, + LogErrorResponseBody = false, + LogErrors = false, + LogRequest = false, + TimeoutMs = 30000, + BufferContent = false + + }, "POST").ConfigureAwait(false)) + { + using (var reader = new StreamReader(response.Content)) + { + var result = reader.ReadToEnd(); + var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase); + + _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid); + //Logger.Debug("Ping test result to {0}. Success: {1}", apiUrl, valid); + return valid; + } + } + } + catch + { + //Logger.Debug("Ping test result to {0}. Success: {1}", apiUrl, false); + + _validAddressResults.AddOrUpdate(apiUrl, false, (k, v) => false); + return false; + } + } + + public string FriendlyName + { + get + { + return string.IsNullOrWhiteSpace(ServerConfigurationManager.Configuration.ServerName) + ? Environment.MachineName + : ServerConfigurationManager.Configuration.ServerName; + } + } + + public int HttpPort { get; private set; } + + public int HttpsPort { get; private set; } + + /// <summary> + /// Gets the mac address. + /// </summary> + /// <returns>System.String.</returns> + private string GetMacAddress() + { + try + { + return NetworkManager.GetMacAddress(); + } + catch (Exception ex) + { + Logger.ErrorException("Error getting mac address", ex); + return null; + } + } + + /// <summary> + /// Shuts down. + /// </summary> + public override async Task Shutdown() + { + try + { + await SessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.ErrorException("Error sending server shutdown notification", ex); + } + + ShutdownInternal(); + } + + protected abstract void ShutdownInternal(); + + /// <summary> + /// Registers the server with administrator access. + /// </summary> + private void RegisterServerWithAdministratorAccess() + { + Logger.Info("Requesting administrative access to authorize http server"); + + try + { + AuthorizeServer(); + } + catch (NotImplementedException) + { + + } + catch (Exception ex) + { + Logger.ErrorException("Error authorizing server", ex); + } + } + + protected virtual void AuthorizeServer() + { + throw new NotImplementedException(); + } + + public event EventHandler HasUpdateAvailableChanged; + + private bool _hasUpdateAvailable; + public bool HasUpdateAvailable + { + get { return _hasUpdateAvailable; } + set + { + var fireEvent = value && !_hasUpdateAvailable; + + _hasUpdateAvailable = value; + + if (fireEvent) + { + EventHelper.FireEventIfNotNull(HasUpdateAvailableChanged, this, EventArgs.Empty, Logger); + } + } + } + + /// <summary> + /// Checks for update. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="progress">The progress.</param> + /// <returns>Task{CheckForUpdateResult}.</returns> + public override async Task<CheckForUpdateResult> CheckForApplicationUpdate(CancellationToken cancellationToken, IProgress<double> progress) + { + var cacheLength = TimeSpan.FromHours(1); + var updateLevel = SystemUpdateLevel; + + if (updateLevel != PackageVersionClass.Release) + { + cacheLength = TimeSpan.FromMinutes(5); + } + + var result = await new GithubUpdater(HttpClient, JsonSerializer).CheckForUpdateResult("MediaBrowser", "Emby", ApplicationVersion, updateLevel, _releaseAssetFilename, + "MBServer", "Mbserver.zip", cacheLength, cancellationToken).ConfigureAwait(false); + + HasUpdateAvailable = result.IsUpdateAvailable; + + return result; + } + + /// <summary> + /// Updates the application. + /// </summary> + /// <param name="package">The package that contains the update</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="progress">The progress.</param> + public override async Task UpdateApplication(PackageVersionInfo package, CancellationToken cancellationToken, IProgress<double> progress) + { + await InstallationManager.InstallPackage(package, false, progress, cancellationToken).ConfigureAwait(false); + + HasUpdateAvailable = false; + + OnApplicationUpdated(package); + } + + /// <summary> + /// Configures the automatic run at startup. + /// </summary> + /// <param name="autorun">if set to <c>true</c> [autorun].</param> + protected override void ConfigureAutoRunAtStartup(bool autorun) + { + if (SupportsAutoRunAtStartup) + { + ConfigureAutoRunInternal(autorun); + } + } + + protected virtual void ConfigureAutoRunInternal(bool autorun) + { + throw new NotImplementedException(); + } + + /// <summary> + /// This returns localhost in the case of no external dns, and the hostname if the + /// dns is prefixed with a valid Uri prefix. + /// </summary> + /// <param name="externalDns">The external dns prefix to get the hostname of.</param> + /// <returns>The hostname in <paramref name="externalDns"/></returns> + private static string GetHostnameFromExternalDns(string externalDns) + { + if (string.IsNullOrWhiteSpace(externalDns)) + { + return "localhost"; + } + + try + { + return new Uri(externalDns).Host; + } + catch + { + return externalDns; + } + } + + public void LaunchUrl(string url) + { + if (EnvironmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows && + EnvironmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.OSX) + { + throw new NotImplementedException(); + } + + var process = ProcessFactory.Create(new ProcessOptions + { + FileName = url, + //EnableRaisingEvents = true, + UseShellExecute = true, + ErrorDialog = false + }); + + process.Exited += ProcessExited; + + try + { + process.Start(); + } + catch (Exception ex) + { + Console.WriteLine("Error launching url: {0}", url); + Logger.ErrorException("Error launching url: {0}", ex, url); + + throw; + } + } + + private static void ProcessExited(object sender, EventArgs e) + { + ((IProcess)sender).Dispose(); + } + + public virtual void EnableLoopback(string appName) + { + } + + private void RegisterModules() + { + var moduleTypes = GetExportTypes<IDependencyModule>(); + + foreach (var type in moduleTypes) + { + try + { + var instance = Activator.CreateInstance(type) as IDependencyModule; + if (instance != null) + instance.BindDependencies(this); + } + catch (Exception ex) + { + Logger.ErrorException("Error setting up dependency bindings for " + type.Name, ex); + } + } + } + + void IDependencyContainer.RegisterSingleInstance<T>(T obj, bool manageLifetime) + { + RegisterSingleInstance(obj, manageLifetime); + } + + void IDependencyContainer.RegisterSingleInstance<T>(Func<T> func) + { + RegisterSingleInstance(func); + } + + void IDependencyContainer.Register(Type typeInterface, Type typeImplementation) + { + Container.Register(typeInterface, typeImplementation); + } + } + + internal class CertificateInfo + { + public string Path { get; set; } + public string Password { get; set; } + } +} diff --git a/Emby.Server.Implementations/ApplicationPathHelper.cs b/Emby.Server.Implementations/ApplicationPathHelper.cs new file mode 100644 index 000000000..262cc526e --- /dev/null +++ b/Emby.Server.Implementations/ApplicationPathHelper.cs @@ -0,0 +1,51 @@ +using System; +using System.Configuration; +using System.IO; + +namespace Emby.Server.Implementations +{ + public static class ApplicationPathHelper + { + /// <summary> + /// Gets the path to the application's ProgramDataFolder + /// </summary> + /// <returns>System.String.</returns> + public static string GetProgramDataPath(string applicationPath) + { + var useDebugPath = false; + +#if DEBUG + useDebugPath = true; +#endif + + var programDataPath = useDebugPath ? + ConfigurationManager.AppSettings["DebugProgramDataPath"] : + ConfigurationManager.AppSettings["ReleaseProgramDataPath"]; + + programDataPath = programDataPath.Replace("%ApplicationData%", Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)); + + programDataPath = programDataPath + .Replace('/', Path.DirectorySeparatorChar) + .Replace('\\', Path.DirectorySeparatorChar); + + // If it's a relative path, e.g. "..\" + if (!Path.IsPathRooted(programDataPath)) + { + var path = Path.GetDirectoryName(applicationPath); + + if (string.IsNullOrEmpty(path)) + { + throw new ApplicationException("Unable to determine running assembly location"); + } + + programDataPath = Path.Combine(path, programDataPath); + + programDataPath = Path.GetFullPath(programDataPath); + } + + Directory.CreateDirectory(programDataPath); + + return programDataPath; + } + } +} diff --git a/Emby.Server.Implementations/Channels/ChannelImageProvider.cs b/Emby.Server.Implementations/Channels/ChannelImageProvider.cs index f892b1e62..0c363c585 100644 --- a/Emby.Server.Implementations/Channels/ChannelImageProvider.cs +++ b/Emby.Server.Implementations/Channels/ChannelImageProvider.cs @@ -18,12 +18,12 @@ namespace Emby.Server.Implementations.Channels _channelManager = channelManager; } - public IEnumerable<ImageType> GetSupportedImages(IHasImages item) + public IEnumerable<ImageType> GetSupportedImages(IHasMetadata item) { return GetChannel(item).GetSupportedChannelImages(); } - public Task<DynamicImageResponse> GetImage(IHasImages item, ImageType type, CancellationToken cancellationToken) + public Task<DynamicImageResponse> GetImage(IHasMetadata item, ImageType type, CancellationToken cancellationToken) { var channel = GetChannel(item); @@ -35,12 +35,12 @@ namespace Emby.Server.Implementations.Channels get { return "Channel Image Provider"; } } - public bool Supports(IHasImages item) + public bool Supports(IHasMetadata item) { return item is Channel; } - private IChannel GetChannel(IHasImages item) + private IChannel GetChannel(IHasMetadata item) { var channel = (Channel)item; diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 2adf6a37c..e41e0ea87 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -159,7 +159,7 @@ namespace Emby.Server.Implementations.Channels all = all.Take(query.Limit.Value).ToList(); } - var returnItems = all.ToArray(); + var returnItems = all.ToArray(all.Count); var result = new QueryResult<Channel> { @@ -182,8 +182,10 @@ namespace Emby.Server.Implementations.Channels { }; - var returnItems = (await _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user).ConfigureAwait(false)) - .ToArray(); + var returnList = (await _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user) + .ConfigureAwait(false)); + var returnItems = returnList + .ToArray(returnList.Count); var result = new QueryResult<BaseItemDto> { @@ -567,8 +569,9 @@ namespace Emby.Server.Implementations.Channels Fields = query.Fields.ToList() }; - var returnItems = (await _dtoService.GetBaseItemDtos(items, dtoOptions, user).ConfigureAwait(false)) - .ToArray(); + var returnList = (await _dtoService.GetBaseItemDtos(items, dtoOptions, user).ConfigureAwait(false)); + var returnItems = returnList + .ToArray(returnList.Count); var result = new QueryResult<BaseItemDto> { @@ -676,12 +679,10 @@ namespace Emby.Server.Implementations.Channels internalItems = internalItems.Take(query.Limit.Value).ToArray(); } - var returnItemArray = internalItems.ToArray(); - return new QueryResult<BaseItem> { TotalRecordCount = totalCount, - Items = returnItemArray + Items = internalItems }; } @@ -813,12 +814,10 @@ namespace Emby.Server.Implementations.Channels var internalItems = await Task.WhenAll(itemTasks).ConfigureAwait(false); - var returnItemArray = internalItems.ToArray(); - return new QueryResult<BaseItem> { TotalRecordCount = totalCount, - Items = returnItemArray + Items = internalItems }; } @@ -837,8 +836,10 @@ namespace Emby.Server.Implementations.Channels Fields = query.Fields.ToList() }; - var returnItems = (await _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user).ConfigureAwait(false)) - .ToArray(); + var returnList = (await _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user) + .ConfigureAwait(false)); + var returnItems = returnList + .ToArray(returnList.Count); var result = new QueryResult<BaseItemDto> { @@ -989,8 +990,10 @@ namespace Emby.Server.Implementations.Channels Fields = query.Fields.ToList() }; - var returnItems = (await _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user).ConfigureAwait(false)) - .ToArray(); + var returnList = (await _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user) + .ConfigureAwait(false)); + var returnItems = returnList + .ToArray(returnList.Count); var result = new QueryResult<BaseItemDto> { @@ -1191,7 +1194,7 @@ namespace Emby.Server.Implementations.Channels } } - var returnItemArray = all.ToArray(); + var returnItemArray = all.ToArray(all.Count); RefreshIfNeeded(returnItemArray); return new QueryResult<BaseItem> @@ -1309,7 +1312,7 @@ namespace Emby.Server.Implementations.Channels { item.Name = info.Name; item.Genres = info.Genres; - item.Studios = info.Studios; + item.Studios = info.Studios.ToArray(info.Studios.Count); item.CommunityRating = info.CommunityRating; item.Overview = info.Overview; item.IndexNumber = info.IndexNumber; @@ -1319,7 +1322,7 @@ namespace Emby.Server.Implementations.Channels item.ProviderIds = info.ProviderIds; item.OfficialRating = info.OfficialRating; item.DateCreated = info.DateCreated ?? DateTime.UtcNow; - item.Tags = info.Tags; + item.Tags = info.Tags.ToArray(info.Tags.Count); item.HomePageUrl = info.HomePageUrl; } else if (info.Type == ChannelItemType.Folder && info.FolderType == ChannelFolderType.Container) @@ -1341,7 +1344,7 @@ namespace Emby.Server.Implementations.Channels var hasAlbumArtists = item as IHasAlbumArtist; if (hasAlbumArtists != null) { - hasAlbumArtists.AlbumArtists = info.AlbumArtists; + hasAlbumArtists.AlbumArtists = info.AlbumArtists.ToArray(info.AlbumArtists.Count); } var trailer = item as Trailer; diff --git a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs index 463d276e5..c7378956d 100644 --- a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs +++ b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs @@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.Collections { } - protected override bool Supports(IHasImages item) + protected override bool Supports(IHasMetadata item) { // Right now this is the only way to prevent this image from getting created ahead of internet image providers if (!item.IsLocked) @@ -32,7 +32,7 @@ namespace Emby.Server.Implementations.Collections return base.Supports(item); } - protected override List<BaseItem> GetItemsWithImages(IHasImages item) + protected override List<BaseItem> GetItemsWithImages(IHasMetadata item) { var playlist = (BoxSet)item; @@ -76,7 +76,7 @@ namespace Emby.Server.Implementations.Collections return GetFinalItems(items, 2); } - protected override string CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + protected override string CreateImage(IHasMetadata item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); } diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index 4e5d344a3..5b168f6cc 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -12,6 +12,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.IO; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.Collections { @@ -190,7 +191,9 @@ namespace Emby.Server.Implementations.Collections if (list.Count > 0) { - collection.LinkedChildren.AddRange(list); + var newList = collection.LinkedChildren.ToList(); + newList.AddRange(list); + collection.LinkedChildren = newList.ToArray(newList.Count); collection.UpdateRatingToContent(); @@ -241,9 +244,9 @@ namespace Emby.Server.Implementations.Collections } } - foreach (var child in list) + if (list.Count > 0) { - collection.LinkedChildren.Remove(child); + collection.LinkedChildren = collection.LinkedChildren.Except(list).ToArray(); } collection.UpdateRatingToContent(); diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index 2241e9377..4d9bf0624 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -16,6 +16,7 @@ using MediaBrowser.Model.Events; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.Configuration { @@ -216,7 +217,7 @@ namespace Emby.Server.Implementations.Configuration list.Add(service); - options.DisabledMetadataSavers = list.ToArray(); + options.DisabledMetadataSavers = list.ToArray(list.Count); } } @@ -236,7 +237,7 @@ namespace Emby.Server.Implementations.Configuration list.Add(options); - config.MetadataOptions = list.ToArray(); + config.MetadataOptions = list.ToArray(list.Count); } return options; diff --git a/Emby.Server.Implementations/Cryptography/ASN1.cs b/Emby.Server.Implementations/Cryptography/ASN1.cs new file mode 100644 index 000000000..f5c826436 --- /dev/null +++ b/Emby.Server.Implementations/Cryptography/ASN1.cs @@ -0,0 +1,340 @@ +// +// ASN1.cs: Abstract Syntax Notation 1 - micro-parser and generator +// +// Authors: +// Sebastien Pouliot <sebastien@ximian.com> +// Jesper Pedersen <jep@itplus.dk> +// +// (C) 2002, 2003 Motus Technologies Inc. (http://www.motus.com) +// Copyright (C) 2004 Novell, Inc (http://www.novell.com) +// (C) 2004 IT+ A/S (http://www.itplus.dk) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections; +using System.IO; +using System.Text; + +namespace Emby.Server.Core.Cryptography +{ + + // References: + // a. ITU ASN.1 standards (free download) + // http://www.itu.int/ITU-T/studygroups/com17/languages/ + + public class ASN1 { + + private byte m_nTag; + private byte[] m_aValue; + private ArrayList elist; + + public ASN1 () : this (0x00, null) {} + + public ASN1 (byte tag) : this (tag, null) {} + + public ASN1 (byte tag, byte[] data) + { + m_nTag = tag; + m_aValue = data; + } + + public ASN1 (byte[] data) + { + m_nTag = data [0]; + + int nLenLength = 0; + int nLength = data [1]; + + if (nLength > 0x80) { + // composed length + nLenLength = nLength - 0x80; + nLength = 0; + for (int i = 0; i < nLenLength; i++) { + nLength *= 256; + nLength += data [i + 2]; + } + } + else if (nLength == 0x80) { + // undefined length encoding + throw new NotSupportedException ("Undefined length encoding."); + } + + m_aValue = new byte [nLength]; + Buffer.BlockCopy (data, (2 + nLenLength), m_aValue, 0, nLength); + + if ((m_nTag & 0x20) == 0x20) { + int nStart = (2 + nLenLength); + Decode (data, ref nStart, data.Length); + } + } + + public int Count { + get { + if (elist == null) + return 0; + return elist.Count; + } + } + + public byte Tag { + get { return m_nTag; } + } + + public int Length { + get { + if (m_aValue != null) + return m_aValue.Length; + else + return 0; + } + } + + public byte[] Value { + get { + if (m_aValue == null) + GetBytes (); + return (byte[]) m_aValue.Clone (); + } + set { + if (value != null) + m_aValue = (byte[]) value.Clone (); + } + } + + private bool CompareArray (byte[] array1, byte[] array2) + { + bool bResult = (array1.Length == array2.Length); + if (bResult) { + for (int i = 0; i < array1.Length; i++) { + if (array1[i] != array2[i]) + return false; + } + } + return bResult; + } + + public bool Equals (byte[] asn1) + { + return CompareArray (this.GetBytes (), asn1); + } + + public bool CompareValue (byte[] value) + { + return CompareArray (m_aValue, value); + } + + public ASN1 Add (ASN1 asn1) + { + if (asn1 != null) { + if (elist == null) + elist = new ArrayList (); + elist.Add (asn1); + } + return asn1; + } + + public virtual byte[] GetBytes () + { + byte[] val = null; + + if (Count > 0) { + int esize = 0; + ArrayList al = new ArrayList (); + foreach (ASN1 a in elist) { + byte[] item = a.GetBytes (); + al.Add (item); + esize += item.Length; + } + val = new byte [esize]; + int pos = 0; + for (int i=0; i < elist.Count; i++) { + byte[] item = (byte[]) al[i]; + Buffer.BlockCopy (item, 0, val, pos, item.Length); + pos += item.Length; + } + } else if (m_aValue != null) { + val = m_aValue; + } + + byte[] der; + int nLengthLen = 0; + + if (val != null) { + int nLength = val.Length; + // special for length > 127 + if (nLength > 127) { + if (nLength <= Byte.MaxValue) { + der = new byte [3 + nLength]; + Buffer.BlockCopy (val, 0, der, 3, nLength); + nLengthLen = 0x81; + der[2] = (byte)(nLength); + } + else if (nLength <= UInt16.MaxValue) { + der = new byte [4 + nLength]; + Buffer.BlockCopy (val, 0, der, 4, nLength); + nLengthLen = 0x82; + der[2] = (byte)(nLength >> 8); + der[3] = (byte)(nLength); + } + else if (nLength <= 0xFFFFFF) { + // 24 bits + der = new byte [5 + nLength]; + Buffer.BlockCopy (val, 0, der, 5, nLength); + nLengthLen = 0x83; + der [2] = (byte)(nLength >> 16); + der [3] = (byte)(nLength >> 8); + der [4] = (byte)(nLength); + } + else { + // max (Length is an integer) 32 bits + der = new byte [6 + nLength]; + Buffer.BlockCopy (val, 0, der, 6, nLength); + nLengthLen = 0x84; + der [2] = (byte)(nLength >> 24); + der [3] = (byte)(nLength >> 16); + der [4] = (byte)(nLength >> 8); + der [5] = (byte)(nLength); + } + } + else { + // basic case (no encoding) + der = new byte [2 + nLength]; + Buffer.BlockCopy (val, 0, der, 2, nLength); + nLengthLen = nLength; + } + if (m_aValue == null) + m_aValue = val; + } + else + der = new byte[2]; + + der[0] = m_nTag; + der[1] = (byte)nLengthLen; + + return der; + } + + // Note: Recursive + protected void Decode (byte[] asn1, ref int anPos, int anLength) + { + byte nTag; + int nLength; + byte[] aValue; + + // minimum is 2 bytes (tag + length of 0) + while (anPos < anLength - 1) { + DecodeTLV (asn1, ref anPos, out nTag, out nLength, out aValue); + // sometimes we get trailing 0 + if (nTag == 0) + continue; + + ASN1 elm = Add (new ASN1 (nTag, aValue)); + + if ((nTag & 0x20) == 0x20) { + int nConstructedPos = anPos; + elm.Decode (asn1, ref nConstructedPos, nConstructedPos + nLength); + } + anPos += nLength; // value length + } + } + + // TLV : Tag - Length - Value + protected void DecodeTLV (byte[] asn1, ref int pos, out byte tag, out int length, out byte[] content) + { + tag = asn1 [pos++]; + length = asn1 [pos++]; + + // special case where L contains the Length of the Length + 0x80 + if ((length & 0x80) == 0x80) { + int nLengthLen = length & 0x7F; + length = 0; + for (int i = 0; i < nLengthLen; i++) + length = length * 256 + asn1 [pos++]; + } + + content = new byte [length]; + Buffer.BlockCopy (asn1, pos, content, 0, length); + } + + public ASN1 this [int index] { + get { + try { + if ((elist == null) || (index >= elist.Count)) + return null; + return (ASN1) elist [index]; + } + catch (ArgumentOutOfRangeException) { + return null; + } + } + } + + public ASN1 Element (int index, byte anTag) + { + try { + if ((elist == null) || (index >= elist.Count)) + return null; + + ASN1 elm = (ASN1) elist [index]; + if (elm.Tag == anTag) + return elm; + else + return null; + } + catch (ArgumentOutOfRangeException) { + return null; + } + } + + public override string ToString() + { + StringBuilder hexLine = new StringBuilder (); + + // Add tag + hexLine.AppendFormat ("Tag: {0} {1}", m_nTag.ToString ("X2"), Environment.NewLine); + + // Add length + hexLine.AppendFormat ("Length: {0} {1}", Value.Length, Environment.NewLine); + + // Add value + hexLine.Append ("Value: "); + hexLine.Append (Environment.NewLine); + for (int i = 0; i < Value.Length; i++) { + hexLine.AppendFormat ("{0} ", Value [i].ToString ("X2")); + if ((i+1) % 16 == 0) + hexLine.AppendFormat (Environment.NewLine); + } + return hexLine.ToString (); + } + + public void SaveToFile (string filename) + { + if (filename == null) + throw new ArgumentNullException ("filename"); + + using (FileStream fs = File.Create (filename)) { + byte[] data = GetBytes (); + fs.Write (data, 0, data.Length); + } + } + } +} diff --git a/Emby.Server.Implementations/Cryptography/ASN1Convert.cs b/Emby.Server.Implementations/Cryptography/ASN1Convert.cs new file mode 100644 index 000000000..851d36dc7 --- /dev/null +++ b/Emby.Server.Implementations/Cryptography/ASN1Convert.cs @@ -0,0 +1,207 @@ +// +// ASN1Convert.cs: Abstract Syntax Notation 1 convertion routines +// +// Authors: +// Sebastien Pouliot <sebastien@ximian.com> +// Jesper Pedersen <jep@itplus.dk> +// +// (C) 2003 Motus Technologies Inc. (http://www.motus.com) +// (C) 2004 IT+ A/S (http://www.itplus.dk) +// Copyright (C) 2004-2007 Novell, Inc (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Globalization; +using System.Security.Cryptography; +using System.Text; + +namespace Emby.Server.Core.Cryptography +{ + + // References: + // a. ITU ASN.1 standards (free download) + // http://www.itu.int/ITU-T/studygroups/com17/languages/ + + public static class ASN1Convert { + // RFC3280, section 4.2.1.5 + // CAs conforming to this profile MUST always encode certificate + // validity dates through the year 2049 as UTCTime; certificate validity + // dates in 2050 or later MUST be encoded as GeneralizedTime. + + // Under 1.x this API requires a Local datetime to be provided + // Under 2.0 it will also accept a Utc datetime + static public ASN1 FromDateTime (DateTime dt) + { + if (dt.Year < 2050) { + // UTCTIME + return new ASN1 (0x17, Encoding.ASCII.GetBytes ( + dt.ToUniversalTime ().ToString ("yyMMddHHmmss", + CultureInfo.InvariantCulture) + "Z")); + } + else { + // GENERALIZEDTIME + return new ASN1 (0x18, Encoding.ASCII.GetBytes ( + dt.ToUniversalTime ().ToString ("yyyyMMddHHmmss", + CultureInfo.InvariantCulture) + "Z")); + } + } + + static public ASN1 FromInt32 (Int32 value) + { + byte[] integer = BitConverterLE.GetBytes (value); + Array.Reverse (integer); + int x = 0; + while ((x < integer.Length) && (integer [x] == 0x00)) + x++; + ASN1 asn1 = new ASN1 (0x02); + switch (x) { + case 0: + asn1.Value = integer; + break; + case 4: + asn1.Value = new byte [1]; + break; + default: + byte[] smallerInt = new byte [4 - x]; + Buffer.BlockCopy (integer, x, smallerInt, 0, smallerInt.Length); + asn1.Value = smallerInt; + break; + } + return asn1; + } + + static public ASN1 FromOid (string oid) + { + if (oid == null) + throw new ArgumentNullException ("oid"); + + return new ASN1 (CryptoConfig.EncodeOID (oid)); + } + + static public ASN1 FromUnsignedBigInteger (byte[] big) + { + if (big == null) + throw new ArgumentNullException ("big"); + + // check for numbers that could be interpreted as negative (first bit) + if (big [0] >= 0x80) { + // in thie cas we add a new, empty, byte (position 0) so we're + // sure this will always be interpreted an unsigned integer. + // However we can't feed it into RSAParameters or DSAParameters + int length = big.Length + 1; + byte[] uinteger = new byte [length]; + Buffer.BlockCopy (big, 0, uinteger, 1, length - 1); + big = uinteger; + } + return new ASN1 (0x02, big); + } + + static public int ToInt32 (ASN1 asn1) + { + if (asn1 == null) + throw new ArgumentNullException ("asn1"); + if (asn1.Tag != 0x02) + throw new FormatException ("Only integer can be converted"); + + int x = 0; + for (int i=0; i < asn1.Value.Length; i++) + x = (x << 8) + asn1.Value [i]; + return x; + } + + // Convert a binary encoded OID to human readable string representation of + // an OID (IETF style). Based on DUMPASN1.C from Peter Gutmann. + static public string ToOid (ASN1 asn1) + { + if (asn1 == null) + throw new ArgumentNullException ("asn1"); + + byte[] aOID = asn1.Value; + StringBuilder sb = new StringBuilder (); + // Pick apart the OID + byte x = (byte) (aOID[0] / 40); + byte y = (byte) (aOID[0] % 40); + if (x > 2) { + // Handle special case for large y if x = 2 + y += (byte) ((x - 2) * 40); + x = 2; + } + sb.Append (x.ToString (CultureInfo.InvariantCulture)); + sb.Append ("."); + sb.Append (y.ToString (CultureInfo.InvariantCulture)); + ulong val = 0; + for (x = 1; x < aOID.Length; x++) { + val = ((val << 7) | ((byte) (aOID [x] & 0x7F))); + if ( !((aOID [x] & 0x80) == 0x80)) { + sb.Append ("."); + sb.Append (val.ToString (CultureInfo.InvariantCulture)); + val = 0; + } + } + return sb.ToString (); + } + + static public DateTime ToDateTime (ASN1 time) + { + if (time == null) + throw new ArgumentNullException ("time"); + + string t = Encoding.ASCII.GetString (time.Value); + // to support both UTCTime and GeneralizedTime (and not so common format) + string mask = null; + int year; + switch (t.Length) { + case 11: + // illegal format, still it's supported for compatibility + mask = "yyMMddHHmmZ"; + break; + case 13: + // RFC3280: 4.1.2.5.1 UTCTime + year = Convert.ToInt16 (t.Substring (0, 2), CultureInfo.InvariantCulture); + // Where YY is greater than or equal to 50, the + // year SHALL be interpreted as 19YY; and + // Where YY is less than 50, the year SHALL be + // interpreted as 20YY. + if (year >= 50) + t = "19" + t; + else + t = "20" + t; + mask = "yyyyMMddHHmmssZ"; + break; + case 15: + mask = "yyyyMMddHHmmssZ"; // GeneralizedTime + break; + case 17: + // another illegal format (990630000000+1000), again supported for compatibility + year = Convert.ToInt16 (t.Substring (0, 2), CultureInfo.InvariantCulture); + string century = (year >= 50) ? "19" : "20"; + // ASN.1 (see ITU X.680 section 43.3) deals with offset differently than .NET + char sign = (t[12] == '+') ? '-' : '+'; + t = String.Format ("{0}{1}{2}{3}{4}:{5}{6}", century, t.Substring (0, 12), sign, + t[13], t[14], t[15], t[16]); + mask = "yyyyMMddHHmmsszzz"; + break; + } + return DateTime.ParseExact (t, mask, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal); + } + } +} diff --git a/Emby.Server.Implementations/Cryptography/BitConverterLE.cs b/Emby.Server.Implementations/Cryptography/BitConverterLE.cs new file mode 100644 index 000000000..34e6bf6dc --- /dev/null +++ b/Emby.Server.Implementations/Cryptography/BitConverterLE.cs @@ -0,0 +1,239 @@ +// +// Mono.Security.BitConverterLE.cs +// Like System.BitConverter but always little endian +// +// Author: +// Bernie Solomon +// + +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; + +namespace Emby.Server.Core.Cryptography +{ + internal sealed class BitConverterLE + { + private BitConverterLE () + { + } + + unsafe private static byte[] GetUShortBytes (byte *bytes) + { + if (BitConverter.IsLittleEndian) + return new byte [] { bytes [0], bytes [1] }; + else + return new byte [] { bytes [1], bytes [0] }; + } + + unsafe private static byte[] GetUIntBytes (byte *bytes) + { + if (BitConverter.IsLittleEndian) + return new byte [] { bytes [0], bytes [1], bytes [2], bytes [3] }; + else + return new byte [] { bytes [3], bytes [2], bytes [1], bytes [0] }; + } + + unsafe private static byte[] GetULongBytes (byte *bytes) + { + if (BitConverter.IsLittleEndian) + return new byte [] { bytes [0], bytes [1], bytes [2], bytes [3], + bytes [4], bytes [5], bytes [6], bytes [7] }; + else + return new byte [] { bytes [7], bytes [6], bytes [5], bytes [4], + bytes [3], bytes [2], bytes [1], bytes [0] }; + } + + unsafe internal static byte[] GetBytes (bool value) + { + return new byte [] { value ? (byte)1 : (byte)0 }; + } + + unsafe internal static byte[] GetBytes (char value) + { + return GetUShortBytes ((byte *) &value); + } + + unsafe internal static byte[] GetBytes (short value) + { + return GetUShortBytes ((byte *) &value); + } + + unsafe internal static byte[] GetBytes (int value) + { + return GetUIntBytes ((byte *) &value); + } + + unsafe internal static byte[] GetBytes (long value) + { + return GetULongBytes ((byte *) &value); + } + + unsafe internal static byte[] GetBytes (ushort value) + { + return GetUShortBytes ((byte *) &value); + } + + unsafe internal static byte[] GetBytes (uint value) + { + return GetUIntBytes ((byte *) &value); + } + + unsafe internal static byte[] GetBytes (ulong value) + { + return GetULongBytes ((byte *) &value); + } + + unsafe internal static byte[] GetBytes (float value) + { + return GetUIntBytes ((byte *) &value); + } + + unsafe internal static byte[] GetBytes (double value) + { + return GetULongBytes ((byte *) &value); + } + + unsafe private static void UShortFromBytes (byte *dst, byte[] src, int startIndex) + { + if (BitConverter.IsLittleEndian) { + dst [0] = src [startIndex]; + dst [1] = src [startIndex + 1]; + } else { + dst [0] = src [startIndex + 1]; + dst [1] = src [startIndex]; + } + } + + unsafe private static void UIntFromBytes (byte *dst, byte[] src, int startIndex) + { + if (BitConverter.IsLittleEndian) { + dst [0] = src [startIndex]; + dst [1] = src [startIndex + 1]; + dst [2] = src [startIndex + 2]; + dst [3] = src [startIndex + 3]; + } else { + dst [0] = src [startIndex + 3]; + dst [1] = src [startIndex + 2]; + dst [2] = src [startIndex + 1]; + dst [3] = src [startIndex]; + } + } + + unsafe private static void ULongFromBytes (byte *dst, byte[] src, int startIndex) + { + if (BitConverter.IsLittleEndian) { + for (int i = 0; i < 8; ++i) + dst [i] = src [startIndex + i]; + } else { + for (int i = 0; i < 8; ++i) + dst [i] = src [startIndex + (7 - i)]; + } + } + + unsafe internal static bool ToBoolean (byte[] value, int startIndex) + { + return value [startIndex] != 0; + } + + unsafe internal static char ToChar (byte[] value, int startIndex) + { + char ret; + + UShortFromBytes ((byte *) &ret, value, startIndex); + + return ret; + } + + unsafe internal static short ToInt16 (byte[] value, int startIndex) + { + short ret; + + UShortFromBytes ((byte *) &ret, value, startIndex); + + return ret; + } + + unsafe internal static int ToInt32 (byte[] value, int startIndex) + { + int ret; + + UIntFromBytes ((byte *) &ret, value, startIndex); + + return ret; + } + + unsafe internal static long ToInt64 (byte[] value, int startIndex) + { + long ret; + + ULongFromBytes ((byte *) &ret, value, startIndex); + + return ret; + } + + unsafe internal static ushort ToUInt16 (byte[] value, int startIndex) + { + ushort ret; + + UShortFromBytes ((byte *) &ret, value, startIndex); + + return ret; + } + + unsafe internal static uint ToUInt32 (byte[] value, int startIndex) + { + uint ret; + + UIntFromBytes ((byte *) &ret, value, startIndex); + + return ret; + } + + unsafe internal static ulong ToUInt64 (byte[] value, int startIndex) + { + ulong ret; + + ULongFromBytes ((byte *) &ret, value, startIndex); + + return ret; + } + + unsafe internal static float ToSingle (byte[] value, int startIndex) + { + float ret; + + UIntFromBytes ((byte *) &ret, value, startIndex); + + return ret; + } + + unsafe internal static double ToDouble (byte[] value, int startIndex) + { + double ret; + + ULongFromBytes ((byte *) &ret, value, startIndex); + + return ret; + } + } +} diff --git a/Emby.Server.Implementations/Cryptography/CertificateGenerator.cs b/Emby.Server.Implementations/Cryptography/CertificateGenerator.cs new file mode 100644 index 000000000..2600d7470 --- /dev/null +++ b/Emby.Server.Implementations/Cryptography/CertificateGenerator.cs @@ -0,0 +1,69 @@ +using MediaBrowser.Model.Logging; +using System; +using System.Collections; +using System.Security.Cryptography; + +namespace Emby.Server.Core.Cryptography +{ + public class CertificateGenerator + { + private const string MonoTestRootAgency = "<RSAKeyValue><Modulus>v/4nALBxCE+9JgEC0LnDUvKh6e96PwTpN4Rj+vWnqKT7IAp1iK/JjuqvAg6DQ2vTfv0dTlqffmHH51OyioprcT5nzxcSTsZb/9jcHScG0s3/FRIWnXeLk/fgm7mSYhjUaHNI0m1/NTTktipicjKxo71hGIg9qucCWnDum+Krh/k=</Modulus><Exponent>AQAB</Exponent><P>9jbKxMXEruW2CfZrzhxtull4O8P47+mNsEL+9gf9QsRO1jJ77C+jmzfU6zbzjf8+ViK+q62tCMdC1ZzulwdpXQ==</P><Q>x5+p198l1PkK0Ga2mRh0SIYSykENpY2aLXoyZD/iUpKYAvATm0/wvKNrE4dKJyPCA+y3hfTdgVag+SP9avvDTQ==</Q><DP>ISSjCvXsUfbOGG05eddN1gXxL2pj+jegQRfjpk7RAsnWKvNExzhqd5x+ZuNQyc6QH5wxun54inP4RTUI0P/IaQ==</DP><DQ>R815VQmR3RIbPqzDXzv5j6CSH6fYlcTiQRtkBsUnzhWmkd/y3XmamO+a8zJFjOCCx9CcjpVuGziivBqi65lVPQ==</DQ><InverseQ>iYiu0KwMWI/dyqN3RJYUzuuLj02/oTD1pYpwo2rvNCXU1Q5VscOeu2DpNg1gWqI+1RrRCsEoaTNzXB1xtKNlSw==</InverseQ><D>nIfh1LYF8fjRBgMdAH/zt9UKHWiaCnc+jXzq5tkR8HVSKTVdzitD8bl1JgAfFQD8VjSXiCJqluexy/B5SGrCXQ49c78NIQj0hD+J13Y8/E0fUbW1QYbhj6Ff7oHyhaYe1WOQfkp2t/h+llHOdt1HRf7bt7dUknYp7m8bQKGxoYE=</D></RSAKeyValue>"; + + public static void CreateSelfSignCertificatePfx( + string fileName, + string hostname, + string password, + ILogger logger) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException("fileName"); + } + + byte[] sn = Guid.NewGuid().ToByteArray(); + string subject = string.Format("CN={0}", hostname); + string issuer = subject; + DateTime notBefore = DateTime.Now.AddDays(-2); + DateTime notAfter = DateTime.Now.AddYears(10); + + RSA issuerKey = RSA.Create(); + issuerKey.FromXmlString(MonoTestRootAgency); + RSA subjectKey = RSA.Create(); + + // serial number MUST be positive + if ((sn[0] & 0x80) == 0x80) + sn[0] -= 0x80; + + issuer = subject; + issuerKey = subjectKey; + + X509CertificateBuilder cb = new X509CertificateBuilder(3); + cb.SerialNumber = sn; + cb.IssuerName = issuer; + cb.NotBefore = notBefore; + cb.NotAfter = notAfter; + cb.SubjectName = subject; + cb.SubjectPublicKey = subjectKey; + + // signature + cb.Hash = "SHA256"; + byte[] rawcert = cb.Sign(issuerKey); + + PKCS12 p12 = new PKCS12(); + + + ArrayList list = new ArrayList(); + // we use a fixed array to avoid endianess issues + // (in case some tools requires the ID to be 1). + list.Add(new byte[4] { 1, 0, 0, 0 }); + Hashtable attributes = new Hashtable(1); + attributes.Add(PKCS9.localKeyId, list); + + p12.AddCertificate(new X509Certificate(rawcert), attributes); + p12.Password = password; + + p12.AddPkcs8ShroudedKeyBag(subjectKey, attributes); + p12.SaveToFile(fileName); + } + } +} diff --git a/Emby.Server.Implementations/Cryptography/CryptoConvert.cs b/Emby.Server.Implementations/Cryptography/CryptoConvert.cs new file mode 100644 index 000000000..70a91bfff --- /dev/null +++ b/Emby.Server.Implementations/Cryptography/CryptoConvert.cs @@ -0,0 +1,745 @@ +// +// CryptoConvert.cs - Crypto Convertion Routines +// +// Author: +// Sebastien Pouliot <sebastien@ximian.com> +// +// (C) 2003 Motus Technologies Inc. (http://www.motus.com) +// Copyright (C) 2004-2006 Novell Inc. (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Globalization; +using System.Security.Cryptography; +using System.Text; + +namespace Emby.Server.Core.Cryptography +{ + + public sealed class CryptoConvert { + + private CryptoConvert () + { + } + + static private int ToInt32LE (byte [] bytes, int offset) + { + return (bytes [offset+3] << 24) | (bytes [offset+2] << 16) | (bytes [offset+1] << 8) | bytes [offset]; + } + + static private uint ToUInt32LE (byte [] bytes, int offset) + { + return (uint)((bytes [offset+3] << 24) | (bytes [offset+2] << 16) | (bytes [offset+1] << 8) | bytes [offset]); + } + + static private byte [] GetBytesLE (int val) + { + return new byte [] { + (byte) (val & 0xff), + (byte) ((val >> 8) & 0xff), + (byte) ((val >> 16) & 0xff), + (byte) ((val >> 24) & 0xff) + }; + } + + static private byte[] Trim (byte[] array) + { + for (int i=0; i < array.Length; i++) { + if (array [i] != 0x00) { + byte[] result = new byte [array.Length - i]; + Buffer.BlockCopy (array, i, result, 0, result.Length); + return result; + } + } + return null; + } + + // convert the key from PRIVATEKEYBLOB to RSA + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/security/Security/private_key_blobs.asp + // e.g. SNK files, PVK files + static public RSA FromCapiPrivateKeyBlob (byte[] blob) + { + return FromCapiPrivateKeyBlob (blob, 0); + } + + static public RSA FromCapiPrivateKeyBlob (byte[] blob, int offset) + { + if (blob == null) + throw new ArgumentNullException ("blob"); + if (offset >= blob.Length) + throw new ArgumentException ("blob is too small."); + + RSAParameters rsap = new RSAParameters (); + try { + if ((blob [offset] != 0x07) || // PRIVATEKEYBLOB (0x07) + (blob [offset+1] != 0x02) || // Version (0x02) + (blob [offset+2] != 0x00) || // Reserved (word) + (blob [offset+3] != 0x00) || + (ToUInt32LE (blob, offset+8) != 0x32415352)) // DWORD magic = RSA2 + throw new CryptographicException ("Invalid blob header"); + + // ALGID (CALG_RSA_SIGN, CALG_RSA_KEYX, ...) + // int algId = ToInt32LE (blob, offset+4); + + // DWORD bitlen + int bitLen = ToInt32LE (blob, offset+12); + + // DWORD public exponent + byte[] exp = new byte [4]; + Buffer.BlockCopy (blob, offset+16, exp, 0, 4); + Array.Reverse (exp); + rsap.Exponent = Trim (exp); + + int pos = offset+20; + // BYTE modulus[rsapubkey.bitlen/8]; + int byteLen = (bitLen >> 3); + rsap.Modulus = new byte [byteLen]; + Buffer.BlockCopy (blob, pos, rsap.Modulus, 0, byteLen); + Array.Reverse (rsap.Modulus); + pos += byteLen; + + // BYTE prime1[rsapubkey.bitlen/16]; + int byteHalfLen = (byteLen >> 1); + rsap.P = new byte [byteHalfLen]; + Buffer.BlockCopy (blob, pos, rsap.P, 0, byteHalfLen); + Array.Reverse (rsap.P); + pos += byteHalfLen; + + // BYTE prime2[rsapubkey.bitlen/16]; + rsap.Q = new byte [byteHalfLen]; + Buffer.BlockCopy (blob, pos, rsap.Q, 0, byteHalfLen); + Array.Reverse (rsap.Q); + pos += byteHalfLen; + + // BYTE exponent1[rsapubkey.bitlen/16]; + rsap.DP = new byte [byteHalfLen]; + Buffer.BlockCopy (blob, pos, rsap.DP, 0, byteHalfLen); + Array.Reverse (rsap.DP); + pos += byteHalfLen; + + // BYTE exponent2[rsapubkey.bitlen/16]; + rsap.DQ = new byte [byteHalfLen]; + Buffer.BlockCopy (blob, pos, rsap.DQ, 0, byteHalfLen); + Array.Reverse (rsap.DQ); + pos += byteHalfLen; + + // BYTE coefficient[rsapubkey.bitlen/16]; + rsap.InverseQ = new byte [byteHalfLen]; + Buffer.BlockCopy (blob, pos, rsap.InverseQ, 0, byteHalfLen); + Array.Reverse (rsap.InverseQ); + pos += byteHalfLen; + + // ok, this is hackish but CryptoAPI support it so... + // note: only works because CRT is used by default + // http://bugzilla.ximian.com/show_bug.cgi?id=57941 + rsap.D = new byte [byteLen]; // must be allocated + if (pos + byteLen + offset <= blob.Length) { + // BYTE privateExponent[rsapubkey.bitlen/8]; + Buffer.BlockCopy (blob, pos, rsap.D, 0, byteLen); + Array.Reverse (rsap.D); + } + } + catch (Exception e) { + throw new CryptographicException ("Invalid blob.", e); + } + + RSA rsa = null; + try + { + rsa = RSA.Create(); + rsa.ImportParameters(rsap); + } + catch (CryptographicException ce) + { + // this may cause problem when this code is run under + // the SYSTEM identity on Windows (e.g. ASP.NET). See + // http://bugzilla.ximian.com/show_bug.cgi?id=77559 + try + { + CspParameters csp = new CspParameters(); + csp.Flags = CspProviderFlags.UseMachineKeyStore; + rsa = new RSACryptoServiceProvider(csp); + rsa.ImportParameters(rsap); + } + catch + { + // rethrow original, not the later, exception if this fails + throw ce; + } + } + return rsa; + } + + static public DSA FromCapiPrivateKeyBlobDSA (byte[] blob) + { + return FromCapiPrivateKeyBlobDSA (blob, 0); + } + + static public DSA FromCapiPrivateKeyBlobDSA (byte[] blob, int offset) + { + if (blob == null) + throw new ArgumentNullException ("blob"); + if (offset >= blob.Length) + throw new ArgumentException ("blob is too small."); + + DSAParameters dsap = new DSAParameters (); + try { + if ((blob [offset] != 0x07) || // PRIVATEKEYBLOB (0x07) + (blob [offset + 1] != 0x02) || // Version (0x02) + (blob [offset + 2] != 0x00) || // Reserved (word) + (blob [offset + 3] != 0x00) || + (ToUInt32LE (blob, offset + 8) != 0x32535344)) // DWORD magic + throw new CryptographicException ("Invalid blob header"); + + int bitlen = ToInt32LE (blob, offset + 12); + int bytelen = bitlen >> 3; + int pos = offset + 16; + + dsap.P = new byte [bytelen]; + Buffer.BlockCopy (blob, pos, dsap.P, 0, bytelen); + Array.Reverse (dsap.P); + pos += bytelen; + + dsap.Q = new byte [20]; + Buffer.BlockCopy (blob, pos, dsap.Q, 0, 20); + Array.Reverse (dsap.Q); + pos += 20; + + dsap.G = new byte [bytelen]; + Buffer.BlockCopy (blob, pos, dsap.G, 0, bytelen); + Array.Reverse (dsap.G); + pos += bytelen; + + dsap.X = new byte [20]; + Buffer.BlockCopy (blob, pos, dsap.X, 0, 20); + Array.Reverse (dsap.X); + pos += 20; + + dsap.Counter = ToInt32LE (blob, pos); + pos += 4; + + dsap.Seed = new byte [20]; + Buffer.BlockCopy (blob, pos, dsap.Seed, 0, 20); + Array.Reverse (dsap.Seed); + pos += 20; + } + catch (Exception e) { + throw new CryptographicException ("Invalid blob.", e); + } + + DSA dsa = null; + try + { + dsa = (DSA)DSA.Create(); + dsa.ImportParameters(dsap); + } + catch (CryptographicException ce) + { + // this may cause problem when this code is run under + // the SYSTEM identity on Windows (e.g. ASP.NET). See + // http://bugzilla.ximian.com/show_bug.cgi?id=77559 + try + { + CspParameters csp = new CspParameters(); + csp.Flags = CspProviderFlags.UseMachineKeyStore; + dsa = new DSACryptoServiceProvider(csp); + dsa.ImportParameters(dsap); + } + catch + { + // rethrow original, not the later, exception if this fails + throw ce; + } + } + return dsa; + } + + static public byte[] ToCapiPrivateKeyBlob (RSA rsa) + { + RSAParameters p = rsa.ExportParameters (true); + int keyLength = p.Modulus.Length; // in bytes + byte[] blob = new byte [20 + (keyLength << 2) + (keyLength >> 1)]; + + blob [0] = 0x07; // Type - PRIVATEKEYBLOB (0x07) + blob [1] = 0x02; // Version - Always CUR_BLOB_VERSION (0x02) + // [2], [3] // RESERVED - Always 0 + blob [5] = 0x24; // ALGID - Always 00 24 00 00 (for CALG_RSA_SIGN) + blob [8] = 0x52; // Magic - RSA2 (ASCII in hex) + blob [9] = 0x53; + blob [10] = 0x41; + blob [11] = 0x32; + + byte[] bitlen = GetBytesLE (keyLength << 3); + blob [12] = bitlen [0]; // bitlen + blob [13] = bitlen [1]; + blob [14] = bitlen [2]; + blob [15] = bitlen [3]; + + // public exponent (DWORD) + int pos = 16; + int n = p.Exponent.Length; + while (n > 0) + blob [pos++] = p.Exponent [--n]; + // modulus + pos = 20; + byte[] part = p.Modulus; + int len = part.Length; + Array.Reverse (part, 0, len); + Buffer.BlockCopy (part, 0, blob, pos, len); + pos += len; + // private key + part = p.P; + len = part.Length; + Array.Reverse (part, 0, len); + Buffer.BlockCopy (part, 0, blob, pos, len); + pos += len; + + part = p.Q; + len = part.Length; + Array.Reverse (part, 0, len); + Buffer.BlockCopy (part, 0, blob, pos, len); + pos += len; + + part = p.DP; + len = part.Length; + Array.Reverse (part, 0, len); + Buffer.BlockCopy (part, 0, blob, pos, len); + pos += len; + + part = p.DQ; + len = part.Length; + Array.Reverse (part, 0, len); + Buffer.BlockCopy (part, 0, blob, pos, len); + pos += len; + + part = p.InverseQ; + len = part.Length; + Array.Reverse (part, 0, len); + Buffer.BlockCopy (part, 0, blob, pos, len); + pos += len; + + part = p.D; + len = part.Length; + Array.Reverse (part, 0, len); + Buffer.BlockCopy (part, 0, blob, pos, len); + + return blob; + } + + static public byte[] ToCapiPrivateKeyBlob (DSA dsa) + { + DSAParameters p = dsa.ExportParameters (true); + int keyLength = p.P.Length; // in bytes + + // header + P + Q + G + X + count + seed + byte[] blob = new byte [16 + keyLength + 20 + keyLength + 20 + 4 + 20]; + + blob [0] = 0x07; // Type - PRIVATEKEYBLOB (0x07) + blob [1] = 0x02; // Version - Always CUR_BLOB_VERSION (0x02) + // [2], [3] // RESERVED - Always 0 + blob [5] = 0x22; // ALGID + blob [8] = 0x44; // Magic + blob [9] = 0x53; + blob [10] = 0x53; + blob [11] = 0x32; + + byte[] bitlen = GetBytesLE (keyLength << 3); + blob [12] = bitlen [0]; + blob [13] = bitlen [1]; + blob [14] = bitlen [2]; + blob [15] = bitlen [3]; + + int pos = 16; + byte[] part = p.P; + Array.Reverse (part); + Buffer.BlockCopy (part, 0, blob, pos, keyLength); + pos += keyLength; + + part = p.Q; + Array.Reverse (part); + Buffer.BlockCopy (part, 0, blob, pos, 20); + pos += 20; + + part = p.G; + Array.Reverse (part); + Buffer.BlockCopy (part, 0, blob, pos, keyLength); + pos += keyLength; + + part = p.X; + Array.Reverse (part); + Buffer.BlockCopy (part, 0, blob, pos, 20); + pos += 20; + + Buffer.BlockCopy (GetBytesLE (p.Counter), 0, blob, pos, 4); + pos += 4; + + part = p.Seed; + Array.Reverse (part); + Buffer.BlockCopy (part, 0, blob, pos, 20); + + return blob; + } + + static public RSA FromCapiPublicKeyBlob (byte[] blob) + { + return FromCapiPublicKeyBlob (blob, 0); + } + + static public RSA FromCapiPublicKeyBlob (byte[] blob, int offset) + { + if (blob == null) + throw new ArgumentNullException ("blob"); + if (offset >= blob.Length) + throw new ArgumentException ("blob is too small."); + + try { + if ((blob [offset] != 0x06) || // PUBLICKEYBLOB (0x06) + (blob [offset+1] != 0x02) || // Version (0x02) + (blob [offset+2] != 0x00) || // Reserved (word) + (blob [offset+3] != 0x00) || + (ToUInt32LE (blob, offset+8) != 0x31415352)) // DWORD magic = RSA1 + throw new CryptographicException ("Invalid blob header"); + + // ALGID (CALG_RSA_SIGN, CALG_RSA_KEYX, ...) + // int algId = ToInt32LE (blob, offset+4); + + // DWORD bitlen + int bitLen = ToInt32LE (blob, offset+12); + + // DWORD public exponent + RSAParameters rsap = new RSAParameters (); + rsap.Exponent = new byte [3]; + rsap.Exponent [0] = blob [offset+18]; + rsap.Exponent [1] = blob [offset+17]; + rsap.Exponent [2] = blob [offset+16]; + + int pos = offset+20; + // BYTE modulus[rsapubkey.bitlen/8]; + int byteLen = (bitLen >> 3); + rsap.Modulus = new byte [byteLen]; + Buffer.BlockCopy (blob, pos, rsap.Modulus, 0, byteLen); + Array.Reverse (rsap.Modulus); + RSA rsa = null; + try + { + rsa = RSA.Create(); + rsa.ImportParameters(rsap); + } + catch (CryptographicException) + { + // this may cause problem when this code is run under + // the SYSTEM identity on Windows (e.g. ASP.NET). See + // http://bugzilla.ximian.com/show_bug.cgi?id=77559 + CspParameters csp = new CspParameters(); + csp.Flags = CspProviderFlags.UseMachineKeyStore; + rsa = new RSACryptoServiceProvider(csp); + rsa.ImportParameters(rsap); + } + return rsa; + } + catch (Exception e) { + throw new CryptographicException ("Invalid blob.", e); + } + } + + static public DSA FromCapiPublicKeyBlobDSA (byte[] blob) + { + return FromCapiPublicKeyBlobDSA (blob, 0); + } + + static public DSA FromCapiPublicKeyBlobDSA (byte[] blob, int offset) + { + if (blob == null) + throw new ArgumentNullException ("blob"); + if (offset >= blob.Length) + throw new ArgumentException ("blob is too small."); + + try { + if ((blob [offset] != 0x06) || // PUBLICKEYBLOB (0x06) + (blob [offset + 1] != 0x02) || // Version (0x02) + (blob [offset + 2] != 0x00) || // Reserved (word) + (blob [offset + 3] != 0x00) || + (ToUInt32LE (blob, offset + 8) != 0x31535344)) // DWORD magic + throw new CryptographicException ("Invalid blob header"); + + int bitlen = ToInt32LE (blob, offset + 12); + DSAParameters dsap = new DSAParameters (); + int bytelen = bitlen >> 3; + int pos = offset + 16; + + dsap.P = new byte [bytelen]; + Buffer.BlockCopy (blob, pos, dsap.P, 0, bytelen); + Array.Reverse (dsap.P); + pos += bytelen; + + dsap.Q = new byte [20]; + Buffer.BlockCopy (blob, pos, dsap.Q, 0, 20); + Array.Reverse (dsap.Q); + pos += 20; + + dsap.G = new byte [bytelen]; + Buffer.BlockCopy (blob, pos, dsap.G, 0, bytelen); + Array.Reverse (dsap.G); + pos += bytelen; + + dsap.Y = new byte [bytelen]; + Buffer.BlockCopy (blob, pos, dsap.Y, 0, bytelen); + Array.Reverse (dsap.Y); + pos += bytelen; + + dsap.Counter = ToInt32LE (blob, pos); + pos += 4; + + dsap.Seed = new byte [20]; + Buffer.BlockCopy (blob, pos, dsap.Seed, 0, 20); + Array.Reverse (dsap.Seed); + pos += 20; + + DSA dsa = (DSA)DSA.Create (); + dsa.ImportParameters (dsap); + return dsa; + } + catch (Exception e) { + throw new CryptographicException ("Invalid blob.", e); + } + } + + static public byte[] ToCapiPublicKeyBlob (RSA rsa) + { + RSAParameters p = rsa.ExportParameters (false); + int keyLength = p.Modulus.Length; // in bytes + byte[] blob = new byte [20 + keyLength]; + + blob [0] = 0x06; // Type - PUBLICKEYBLOB (0x06) + blob [1] = 0x02; // Version - Always CUR_BLOB_VERSION (0x02) + // [2], [3] // RESERVED - Always 0 + blob [5] = 0x24; // ALGID - Always 00 24 00 00 (for CALG_RSA_SIGN) + blob [8] = 0x52; // Magic - RSA1 (ASCII in hex) + blob [9] = 0x53; + blob [10] = 0x41; + blob [11] = 0x31; + + byte[] bitlen = GetBytesLE (keyLength << 3); + blob [12] = bitlen [0]; // bitlen + blob [13] = bitlen [1]; + blob [14] = bitlen [2]; + blob [15] = bitlen [3]; + + // public exponent (DWORD) + int pos = 16; + int n = p.Exponent.Length; + while (n > 0) + blob [pos++] = p.Exponent [--n]; + // modulus + pos = 20; + byte[] part = p.Modulus; + int len = part.Length; + Array.Reverse (part, 0, len); + Buffer.BlockCopy (part, 0, blob, pos, len); + pos += len; + return blob; + } + + static public byte[] ToCapiPublicKeyBlob (DSA dsa) + { + DSAParameters p = dsa.ExportParameters (false); + int keyLength = p.P.Length; // in bytes + + // header + P + Q + G + Y + count + seed + byte[] blob = new byte [16 + keyLength + 20 + keyLength + keyLength + 4 + 20]; + + blob [0] = 0x06; // Type - PUBLICKEYBLOB (0x06) + blob [1] = 0x02; // Version - Always CUR_BLOB_VERSION (0x02) + // [2], [3] // RESERVED - Always 0 + blob [5] = 0x22; // ALGID + blob [8] = 0x44; // Magic + blob [9] = 0x53; + blob [10] = 0x53; + blob [11] = 0x31; + + byte[] bitlen = GetBytesLE (keyLength << 3); + blob [12] = bitlen [0]; + blob [13] = bitlen [1]; + blob [14] = bitlen [2]; + blob [15] = bitlen [3]; + + int pos = 16; + byte[] part; + + part = p.P; + Array.Reverse (part); + Buffer.BlockCopy (part, 0, blob, pos, keyLength); + pos += keyLength; + + part = p.Q; + Array.Reverse (part); + Buffer.BlockCopy (part, 0, blob, pos, 20); + pos += 20; + + part = p.G; + Array.Reverse (part); + Buffer.BlockCopy (part, 0, blob, pos, keyLength); + pos += keyLength; + + part = p.Y; + Array.Reverse (part); + Buffer.BlockCopy (part, 0, blob, pos, keyLength); + pos += keyLength; + + Buffer.BlockCopy (GetBytesLE (p.Counter), 0, blob, pos, 4); + pos += 4; + + part = p.Seed; + Array.Reverse (part); + Buffer.BlockCopy (part, 0, blob, pos, 20); + + return blob; + } + + // PRIVATEKEYBLOB + // PUBLICKEYBLOB + static public RSA FromCapiKeyBlob (byte[] blob) + { + return FromCapiKeyBlob (blob, 0); + } + + static public RSA FromCapiKeyBlob (byte[] blob, int offset) + { + if (blob == null) + throw new ArgumentNullException ("blob"); + if (offset >= blob.Length) + throw new ArgumentException ("blob is too small."); + + switch (blob [offset]) { + case 0x00: + // this could be a public key inside an header + // like "sn -e" would produce + if (blob [offset + 12] == 0x06) { + return FromCapiPublicKeyBlob (blob, offset + 12); + } + break; + case 0x06: + return FromCapiPublicKeyBlob (blob, offset); + case 0x07: + return FromCapiPrivateKeyBlob (blob, offset); + } + throw new CryptographicException ("Unknown blob format."); + } + + static public DSA FromCapiKeyBlobDSA (byte[] blob) + { + return FromCapiKeyBlobDSA (blob, 0); + } + + static public DSA FromCapiKeyBlobDSA (byte[] blob, int offset) + { + if (blob == null) + throw new ArgumentNullException ("blob"); + if (offset >= blob.Length) + throw new ArgumentException ("blob is too small."); + + switch (blob [offset]) { + case 0x06: + return FromCapiPublicKeyBlobDSA (blob, offset); + case 0x07: + return FromCapiPrivateKeyBlobDSA (blob, offset); + } + throw new CryptographicException ("Unknown blob format."); + } + + static public byte[] ToCapiKeyBlob (AsymmetricAlgorithm keypair, bool includePrivateKey) + { + if (keypair == null) + throw new ArgumentNullException ("keypair"); + + // check between RSA and DSA (and potentially others like DH) + if (keypair is RSA) + return ToCapiKeyBlob ((RSA)keypair, includePrivateKey); + else if (keypair is DSA) + return ToCapiKeyBlob ((DSA)keypair, includePrivateKey); + else + return null; // TODO + } + + static public byte[] ToCapiKeyBlob (RSA rsa, bool includePrivateKey) + { + if (rsa == null) + throw new ArgumentNullException ("rsa"); + + if (includePrivateKey) + return ToCapiPrivateKeyBlob (rsa); + else + return ToCapiPublicKeyBlob (rsa); + } + + static public byte[] ToCapiKeyBlob (DSA dsa, bool includePrivateKey) + { + if (dsa == null) + throw new ArgumentNullException ("dsa"); + + if (includePrivateKey) + return ToCapiPrivateKeyBlob (dsa); + else + return ToCapiPublicKeyBlob (dsa); + } + + static public string ToHex (byte[] input) + { + if (input == null) + return null; + + StringBuilder sb = new StringBuilder (input.Length * 2); + foreach (byte b in input) { + sb.Append (b.ToString ("X2", CultureInfo.InvariantCulture)); + } + return sb.ToString (); + } + + static private byte FromHexChar (char c) + { + if ((c >= 'a') && (c <= 'f')) + return (byte) (c - 'a' + 10); + if ((c >= 'A') && (c <= 'F')) + return (byte) (c - 'A' + 10); + if ((c >= '0') && (c <= '9')) + return (byte) (c - '0'); + throw new ArgumentException ("invalid hex char"); + } + + static public byte[] FromHex (string hex) + { + if (hex == null) + return null; + if ((hex.Length & 0x1) == 0x1) + throw new ArgumentException ("Length must be a multiple of 2"); + + byte[] result = new byte [hex.Length >> 1]; + int n = 0; + int i = 0; + while (n < result.Length) { + result [n] = (byte) (FromHexChar (hex [i++]) << 4); + result [n++] += FromHexChar (hex [i++]); + } + return result; + } + } +} diff --git a/Emby.Server.Implementations/Cryptography/PKCS1.cs b/Emby.Server.Implementations/Cryptography/PKCS1.cs new file mode 100644 index 000000000..24c0708c5 --- /dev/null +++ b/Emby.Server.Implementations/Cryptography/PKCS1.cs @@ -0,0 +1,491 @@ +// +// PKCS1.cs - Implements PKCS#1 primitives. +// +// Author: +// Sebastien Pouliot <sebastien@xamarin.com> +// +// (C) 2002, 2003 Motus Technologies Inc. (http://www.motus.com) +// Copyright (C) 2004 Novell, Inc (http://www.novell.com) +// Copyright 2013 Xamarin Inc. (http://www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Security.Cryptography; + +namespace Emby.Server.Core.Cryptography +{ + + // References: + // a. PKCS#1: RSA Cryptography Standard + // http://www.rsasecurity.com/rsalabs/pkcs/pkcs-1/index.html + + public sealed class PKCS1 { + + private PKCS1 () + { + } + + private static bool Compare (byte[] array1, byte[] array2) + { + bool result = (array1.Length == array2.Length); + if (result) { + for (int i=0; i < array1.Length; i++) + if (array1[i] != array2[i]) + return false; + } + return result; + } + + private static byte[] xor (byte[] array1, byte[] array2) + { + byte[] result = new byte [array1.Length]; + for (int i=0; i < result.Length; i++) + result[i] = (byte) (array1[i] ^ array2[i]); + return result; + } + + private static byte[] emptySHA1 = { 0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, 0x32, 0x55, 0xbf, 0xef, 0x95, 0x60, 0x18, 0x90, 0xaf, 0xd8, 0x07, 0x09 }; + private static byte[] emptySHA256 = { 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55 }; + private static byte[] emptySHA384 = { 0x38, 0xb0, 0x60, 0xa7, 0x51, 0xac, 0x96, 0x38, 0x4c, 0xd9, 0x32, 0x7e, 0xb1, 0xb1, 0xe3, 0x6a, 0x21, 0xfd, 0xb7, 0x11, 0x14, 0xbe, 0x07, 0x43, 0x4c, 0x0c, 0xc7, 0xbf, 0x63, 0xf6, 0xe1, 0xda, 0x27, 0x4e, 0xde, 0xbf, 0xe7, 0x6f, 0x65, 0xfb, 0xd5, 0x1a, 0xd2, 0xf1, 0x48, 0x98, 0xb9, 0x5b }; + private static byte[] emptySHA512 = { 0xcf, 0x83, 0xe1, 0x35, 0x7e, 0xef, 0xb8, 0xbd, 0xf1, 0x54, 0x28, 0x50, 0xd6, 0x6d, 0x80, 0x07, 0xd6, 0x20, 0xe4, 0x05, 0x0b, 0x57, 0x15, 0xdc, 0x83, 0xf4, 0xa9, 0x21, 0xd3, 0x6c, 0xe9, 0xce, 0x47, 0xd0, 0xd1, 0x3c, 0x5d, 0x85, 0xf2, 0xb0, 0xff, 0x83, 0x18, 0xd2, 0x87, 0x7e, 0xec, 0x2f, 0x63, 0xb9, 0x31, 0xbd, 0x47, 0x41, 0x7a, 0x81, 0xa5, 0x38, 0x32, 0x7a, 0xf9, 0x27, 0xda, 0x3e }; + + private static byte[] GetEmptyHash (HashAlgorithm hash) + { + if (hash is SHA1) + return emptySHA1; + else if (hash is SHA256) + return emptySHA256; + else if (hash is SHA384) + return emptySHA384; + else if (hash is SHA512) + return emptySHA512; + else + return hash.ComputeHash ((byte[])null); + } + + // PKCS #1 v.2.1, Section 4.1 + // I2OSP converts a non-negative integer to an octet string of a specified length. + public static byte[] I2OSP (int x, int size) + { + byte[] array = BitConverterLE.GetBytes (x); + Array.Reverse (array, 0, array.Length); + return I2OSP (array, size); + } + + public static byte[] I2OSP (byte[] x, int size) + { + byte[] result = new byte [size]; + Buffer.BlockCopy (x, 0, result, (result.Length - x.Length), x.Length); + return result; + } + + // PKCS #1 v.2.1, Section 4.2 + // OS2IP converts an octet string to a nonnegative integer. + public static byte[] OS2IP (byte[] x) + { + int i = 0; + while ((x [i++] == 0x00) && (i < x.Length)) { + // confuse compiler into reporting a warning with {} + } + i--; + if (i > 0) { + byte[] result = new byte [x.Length - i]; + Buffer.BlockCopy (x, i, result, 0, result.Length); + return result; + } + else + return x; + } + + // PKCS #1 v.2.1, Section 5.1.1 + public static byte[] RSAEP (RSA rsa, byte[] m) + { + // c = m^e mod n + return rsa.EncryptValue (m); + } + + // PKCS #1 v.2.1, Section 5.1.2 + public static byte[] RSADP (RSA rsa, byte[] c) + { + // m = c^d mod n + // Decrypt value may apply CRT optimizations + return rsa.DecryptValue (c); + } + + // PKCS #1 v.2.1, Section 5.2.1 + public static byte[] RSASP1 (RSA rsa, byte[] m) + { + // first form: s = m^d mod n + // Decrypt value may apply CRT optimizations + return rsa.DecryptValue (m); + } + + // PKCS #1 v.2.1, Section 5.2.2 + public static byte[] RSAVP1 (RSA rsa, byte[] s) + { + // m = s^e mod n + return rsa.EncryptValue (s); + } + + // PKCS #1 v.2.1, Section 7.1.1 + // RSAES-OAEP-ENCRYPT ((n, e), M, L) + public static byte[] Encrypt_OAEP (RSA rsa, HashAlgorithm hash, RandomNumberGenerator rng, byte[] M) + { + int size = rsa.KeySize / 8; + int hLen = hash.HashSize / 8; + if (M.Length > size - 2 * hLen - 2) + throw new CryptographicException ("message too long"); + // empty label L SHA1 hash + byte[] lHash = GetEmptyHash (hash); + int PSLength = (size - M.Length - 2 * hLen - 2); + // DB = lHash || PS || 0x01 || M + byte[] DB = new byte [lHash.Length + PSLength + 1 + M.Length]; + Buffer.BlockCopy (lHash, 0, DB, 0, lHash.Length); + DB [(lHash.Length + PSLength)] = 0x01; + Buffer.BlockCopy (M, 0, DB, (DB.Length - M.Length), M.Length); + + byte[] seed = new byte [hLen]; + rng.GetBytes (seed); + + byte[] dbMask = MGF1 (hash, seed, size - hLen - 1); + byte[] maskedDB = xor (DB, dbMask); + byte[] seedMask = MGF1 (hash, maskedDB, hLen); + byte[] maskedSeed = xor (seed, seedMask); + // EM = 0x00 || maskedSeed || maskedDB + byte[] EM = new byte [maskedSeed.Length + maskedDB.Length + 1]; + Buffer.BlockCopy (maskedSeed, 0, EM, 1, maskedSeed.Length); + Buffer.BlockCopy (maskedDB, 0, EM, maskedSeed.Length + 1, maskedDB.Length); + + byte[] m = OS2IP (EM); + byte[] c = RSAEP (rsa, m); + return I2OSP (c, size); + } + + // PKCS #1 v.2.1, Section 7.1.2 + // RSAES-OAEP-DECRYPT (K, C, L) + public static byte[] Decrypt_OAEP (RSA rsa, HashAlgorithm hash, byte[] C) + { + int size = rsa.KeySize / 8; + int hLen = hash.HashSize / 8; + if ((size < (2 * hLen + 2)) || (C.Length != size)) + throw new CryptographicException ("decryption error"); + + byte[] c = OS2IP (C); + byte[] m = RSADP (rsa, c); + byte[] EM = I2OSP (m, size); + + // split EM = Y || maskedSeed || maskedDB + byte[] maskedSeed = new byte [hLen]; + Buffer.BlockCopy (EM, 1, maskedSeed, 0, maskedSeed.Length); + byte[] maskedDB = new byte [size - hLen - 1]; + Buffer.BlockCopy (EM, (EM.Length - maskedDB.Length), maskedDB, 0, maskedDB.Length); + + byte[] seedMask = MGF1 (hash, maskedDB, hLen); + byte[] seed = xor (maskedSeed, seedMask); + byte[] dbMask = MGF1 (hash, seed, size - hLen - 1); + byte[] DB = xor (maskedDB, dbMask); + + byte[] lHash = GetEmptyHash (hash); + // split DB = lHash' || PS || 0x01 || M + byte[] dbHash = new byte [lHash.Length]; + Buffer.BlockCopy (DB, 0, dbHash, 0, dbHash.Length); + bool h = Compare (lHash, dbHash); + + // find separator 0x01 + int nPos = lHash.Length; + while (DB[nPos] == 0) + nPos++; + + int Msize = DB.Length - nPos - 1; + byte[] M = new byte [Msize]; + Buffer.BlockCopy (DB, (nPos + 1), M, 0, Msize); + + // we could have returned EM[0] sooner but would be helping a timing attack + if ((EM[0] != 0) || (!h) || (DB[nPos] != 0x01)) + return null; + return M; + } + + // PKCS #1 v.2.1, Section 7.2.1 + // RSAES-PKCS1-V1_5-ENCRYPT ((n, e), M) + public static byte[] Encrypt_v15 (RSA rsa, RandomNumberGenerator rng, byte[] M) + { + int size = rsa.KeySize / 8; + if (M.Length > size - 11) + throw new CryptographicException ("message too long"); + int PSLength = System.Math.Max (8, (size - M.Length - 3)); + byte[] PS = new byte [PSLength]; + rng.GetNonZeroBytes (PS); + byte[] EM = new byte [size]; + EM [1] = 0x02; + Buffer.BlockCopy (PS, 0, EM, 2, PSLength); + Buffer.BlockCopy (M, 0, EM, (size - M.Length), M.Length); + + byte[] m = OS2IP (EM); + byte[] c = RSAEP (rsa, m); + byte[] C = I2OSP (c, size); + return C; + } + + // PKCS #1 v.2.1, Section 7.2.2 + // RSAES-PKCS1-V1_5-DECRYPT (K, C) + public static byte[] Decrypt_v15 (RSA rsa, byte[] C) + { + int size = rsa.KeySize >> 3; // div by 8 + if ((size < 11) || (C.Length > size)) + throw new CryptographicException ("decryption error"); + byte[] c = OS2IP (C); + byte[] m = RSADP (rsa, c); + byte[] EM = I2OSP (m, size); + + if ((EM [0] != 0x00) || (EM [1] != 0x02)) + return null; + + int mPos = 10; + // PS is a minimum of 8 bytes + 2 bytes for header + while ((EM [mPos] != 0x00) && (mPos < EM.Length)) + mPos++; + if (EM [mPos] != 0x00) + return null; + mPos++; + byte[] M = new byte [EM.Length - mPos]; + Buffer.BlockCopy (EM, mPos, M, 0, M.Length); + return M; + } + + // PKCS #1 v.2.1, Section 8.2.1 + // RSASSA-PKCS1-V1_5-SIGN (K, M) + public static byte[] Sign_v15 (RSA rsa, HashAlgorithm hash, byte[] hashValue) + { + int size = (rsa.KeySize >> 3); // div 8 + byte[] EM = Encode_v15 (hash, hashValue, size); + byte[] m = OS2IP (EM); + byte[] s = RSASP1 (rsa, m); + byte[] S = I2OSP (s, size); + return S; + } + + internal static byte[] Sign_v15 (RSA rsa, string hashName, byte[] hashValue) + { + using (var hash = CreateFromName (hashName)) + return Sign_v15 (rsa, hash, hashValue); + } + + // PKCS #1 v.2.1, Section 8.2.2 + // RSASSA-PKCS1-V1_5-VERIFY ((n, e), M, S) + public static bool Verify_v15 (RSA rsa, HashAlgorithm hash, byte[] hashValue, byte[] signature) + { + return Verify_v15 (rsa, hash, hashValue, signature, false); + } + + internal static bool Verify_v15 (RSA rsa, string hashName, byte[] hashValue, byte[] signature) + { + using (var hash = CreateFromName (hashName)) + return Verify_v15 (rsa, hash, hashValue, signature, false); + } + + // DO NOT USE WITHOUT A VERY GOOD REASON + public static bool Verify_v15 (RSA rsa, HashAlgorithm hash, byte [] hashValue, byte [] signature, bool tryNonStandardEncoding) + { + int size = (rsa.KeySize >> 3); // div 8 + byte[] s = OS2IP (signature); + byte[] m = RSAVP1 (rsa, s); + byte[] EM2 = I2OSP (m, size); + byte[] EM = Encode_v15 (hash, hashValue, size); + bool result = Compare (EM, EM2); + if (result || !tryNonStandardEncoding) + return result; + + // NOTE: some signatures don't include the hash OID (pretty lame but real) + // and compatible with MS implementation. E.g. Verisign Authenticode Timestamps + + // we're making this "as safe as possible" + if ((EM2 [0] != 0x00) || (EM2 [1] != 0x01)) + return false; + int i; + for (i = 2; i < EM2.Length - hashValue.Length - 1; i++) { + if (EM2 [i] != 0xFF) + return false; + } + if (EM2 [i++] != 0x00) + return false; + + byte [] decryptedHash = new byte [hashValue.Length]; + Buffer.BlockCopy (EM2, i, decryptedHash, 0, decryptedHash.Length); + return Compare (decryptedHash, hashValue); + } + + // PKCS #1 v.2.1, Section 9.2 + // EMSA-PKCS1-v1_5-Encode + public static byte[] Encode_v15 (HashAlgorithm hash, byte[] hashValue, int emLength) + { + if (hashValue.Length != (hash.HashSize >> 3)) + throw new CryptographicException ("bad hash length for " + hash.ToString ()); + + // DigestInfo ::= SEQUENCE { + // digestAlgorithm AlgorithmIdentifier, + // digest OCTET STRING + // } + + byte[] t = null; + + string oid = CryptoConfig.MapNameToOID (hash.ToString ()); + if (oid != null) + { + ASN1 digestAlgorithm = new ASN1 (0x30); + digestAlgorithm.Add (new ASN1 (CryptoConfig.EncodeOID (oid))); + digestAlgorithm.Add (new ASN1 (0x05)); // NULL + ASN1 digest = new ASN1 (0x04, hashValue); + ASN1 digestInfo = new ASN1 (0x30); + digestInfo.Add (digestAlgorithm); + digestInfo.Add (digest); + + t = digestInfo.GetBytes (); + } + else + { + // There are no valid OID, in this case t = hashValue + // This is the case of the MD5SHA hash algorithm + t = hashValue; + } + + Buffer.BlockCopy (hashValue, 0, t, t.Length - hashValue.Length, hashValue.Length); + + int PSLength = System.Math.Max (8, emLength - t.Length - 3); + // PS = PSLength of 0xff + + // EM = 0x00 | 0x01 | PS | 0x00 | T + byte[] EM = new byte [PSLength + t.Length + 3]; + EM [1] = 0x01; + for (int i=2; i < PSLength + 2; i++) + EM[i] = 0xff; + Buffer.BlockCopy (t, 0, EM, PSLength + 3, t.Length); + + return EM; + } + + // PKCS #1 v.2.1, Section B.2.1 + public static byte[] MGF1 (HashAlgorithm hash, byte[] mgfSeed, int maskLen) + { + // 1. If maskLen > 2^32 hLen, output "mask too long" and stop. + // easy - this is impossible by using a int (31bits) as parameter ;-) + // BUT with a signed int we do have to check for negative values! + if (maskLen < 0) + throw new OverflowException(); + + int mgfSeedLength = mgfSeed.Length; + int hLen = (hash.HashSize >> 3); // from bits to bytes + int iterations = (maskLen / hLen); + if (maskLen % hLen != 0) + iterations++; + // 2. Let T be the empty octet string. + byte[] T = new byte [iterations * hLen]; + + byte[] toBeHashed = new byte [mgfSeedLength + 4]; + int pos = 0; + // 3. For counter from 0 to \ceil (maskLen / hLen) - 1, do the following: + for (int counter = 0; counter < iterations; counter++) { + // a. Convert counter to an octet string C of length 4 octets + byte[] C = I2OSP (counter, 4); + + // b. Concatenate the hash of the seed mgfSeed and C to the octet string T: + // T = T || Hash (mgfSeed || C) + Buffer.BlockCopy (mgfSeed, 0, toBeHashed, 0, mgfSeedLength); + Buffer.BlockCopy (C, 0, toBeHashed, mgfSeedLength, 4); + byte[] output = hash.ComputeHash (toBeHashed); + Buffer.BlockCopy (output, 0, T, pos, hLen); + pos += hLen; + } + + // 4. Output the leading maskLen octets of T as the octet string mask. + byte[] mask = new byte [maskLen]; + Buffer.BlockCopy (T, 0, mask, 0, maskLen); + return mask; + } + + static internal string HashNameFromOid (string oid, bool throwOnError = true) + { + switch (oid) { + case "1.2.840.113549.1.1.2": // MD2 with RSA encryption + return "MD2"; + case "1.2.840.113549.1.1.3": // MD4 with RSA encryption + return "MD4"; + case "1.2.840.113549.1.1.4": // MD5 with RSA encryption + return "MD5"; + case "1.2.840.113549.1.1.5": // SHA-1 with RSA Encryption + case "1.3.14.3.2.29": // SHA1 with RSA signature + case "1.2.840.10040.4.3": // SHA1-1 with DSA + return "SHA1"; + case "1.2.840.113549.1.1.11": // SHA-256 with RSA Encryption + return "SHA256"; + case "1.2.840.113549.1.1.12": // SHA-384 with RSA Encryption + return "SHA384"; + case "1.2.840.113549.1.1.13": // SHA-512 with RSA Encryption + return "SHA512"; + case "1.3.36.3.3.1.2": + return "RIPEMD160"; + default: + if (throwOnError) + throw new CryptographicException ("Unsupported hash algorithm: " + oid); + return null; + } + } + + static internal HashAlgorithm CreateFromOid (string oid) + { + return CreateFromName (HashNameFromOid (oid)); + } + + static internal HashAlgorithm CreateFromName (string name) + { +#if FULL_AOT_RUNTIME + switch (name) { + case "MD2": + return MD2.Create (); + case "MD4": + return MD4.Create (); + case "MD5": + return MD5.Create (); + case "SHA1": + return SHA1.Create (); + case "SHA256": + return SHA256.Create (); + case "SHA384": + return SHA384.Create (); + case "SHA512": + return SHA512.Create (); + case "RIPEMD160": + return RIPEMD160.Create (); + default: + try { + return (HashAlgorithm) Activator.CreateInstance (Type.GetType (name)); + } + catch { + throw new CryptographicException ("Unsupported hash algorithm: " + name); + } + } +#else + return HashAlgorithm.Create (name); +#endif + } + } +} diff --git a/Emby.Server.Implementations/Cryptography/PKCS12.cs b/Emby.Server.Implementations/Cryptography/PKCS12.cs new file mode 100644 index 000000000..50f3776d9 --- /dev/null +++ b/Emby.Server.Implementations/Cryptography/PKCS12.cs @@ -0,0 +1,1934 @@ +// +// PKCS12.cs: PKCS 12 - Personal Information Exchange Syntax +// +// Author: +// Sebastien Pouliot <sebastien@xamarin.com> +// +// (C) 2003 Motus Technologies Inc. (http://www.motus.com) +// Copyright (C) 2004,2005,2006 Novell Inc. (http://www.novell.com) +// Copyright 2013 Xamarin Inc. (http://www.xamarin.com) +// +// Key derivation translated from Bouncy Castle JCE (http://www.bouncycastle.org/) +// See bouncycastle.txt for license. +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace Emby.Server.Core.Cryptography +{ + + public class PKCS5 { + + public const string pbeWithMD2AndDESCBC = "1.2.840.113549.1.5.1"; + public const string pbeWithMD5AndDESCBC = "1.2.840.113549.1.5.3"; + public const string pbeWithMD2AndRC2CBC = "1.2.840.113549.1.5.4"; + public const string pbeWithMD5AndRC2CBC = "1.2.840.113549.1.5.6"; + public const string pbeWithSHA1AndDESCBC = "1.2.840.113549.1.5.10"; + public const string pbeWithSHA1AndRC2CBC = "1.2.840.113549.1.5.11"; + + public PKCS5 () {} + } + + public class PKCS9 { + + public const string friendlyName = "1.2.840.113549.1.9.20"; + public const string localKeyId = "1.2.840.113549.1.9.21"; + + public PKCS9 () {} + } + + + internal class SafeBag { + private string _bagOID; + private ASN1 _asn1; + + public SafeBag(string bagOID, ASN1 asn1) { + _bagOID = bagOID; + _asn1 = asn1; + } + + public string BagOID { + get { return _bagOID; } + } + + public ASN1 ASN1 { + get { return _asn1; } + } + } + + + public class PKCS12 : ICloneable { + + public const string pbeWithSHAAnd128BitRC4 = "1.2.840.113549.1.12.1.1"; + public const string pbeWithSHAAnd40BitRC4 = "1.2.840.113549.1.12.1.2"; + public const string pbeWithSHAAnd3KeyTripleDESCBC = "1.2.840.113549.1.12.1.3"; + public const string pbeWithSHAAnd2KeyTripleDESCBC = "1.2.840.113549.1.12.1.4"; + public const string pbeWithSHAAnd128BitRC2CBC = "1.2.840.113549.1.12.1.5"; + public const string pbeWithSHAAnd40BitRC2CBC = "1.2.840.113549.1.12.1.6"; + + // bags + public const string keyBag = "1.2.840.113549.1.12.10.1.1"; + public const string pkcs8ShroudedKeyBag = "1.2.840.113549.1.12.10.1.2"; + public const string certBag = "1.2.840.113549.1.12.10.1.3"; + public const string crlBag = "1.2.840.113549.1.12.10.1.4"; + public const string secretBag = "1.2.840.113549.1.12.10.1.5"; + public const string safeContentsBag = "1.2.840.113549.1.12.10.1.6"; + + // types + public const string x509Certificate = "1.2.840.113549.1.9.22.1"; + public const string sdsiCertificate = "1.2.840.113549.1.9.22.2"; + public const string x509Crl = "1.2.840.113549.1.9.23.1"; + + // Adapted from BouncyCastle PKCS12ParametersGenerator.java + public class DeriveBytes { + + public enum Purpose { + Key, + IV, + MAC + } + + static private byte[] keyDiversifier = { 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 }; + static private byte[] ivDiversifier = { 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2 }; + static private byte[] macDiversifier = { 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3 }; + + private string _hashName; + private int _iterations; + private byte[] _password; + private byte[] _salt; + + public DeriveBytes () {} + + public string HashName { + get { return _hashName; } + set { _hashName = value; } + } + + public int IterationCount { + get { return _iterations; } + set { _iterations = value; } + } + + public byte[] Password { + get { return (byte[]) _password.Clone (); } + set { + if (value == null) + _password = new byte [0]; + else + _password = (byte[]) value.Clone (); + } + } + + public byte[] Salt { + get { return (byte[]) _salt.Clone (); } + set { + if (value != null) + _salt = (byte[]) value.Clone (); + else + _salt = null; + } + } + + private void Adjust (byte[] a, int aOff, byte[] b) + { + int x = (b[b.Length - 1] & 0xff) + (a [aOff + b.Length - 1] & 0xff) + 1; + + a [aOff + b.Length - 1] = (byte) x; + x >>= 8; + + for (int i = b.Length - 2; i >= 0; i--) { + x += (b [i] & 0xff) + (a [aOff + i] & 0xff); + a [aOff + i] = (byte) x; + x >>= 8; + } + } + + private byte[] Derive (byte[] diversifier, int n) + { + HashAlgorithm digest = PKCS1.CreateFromName (_hashName); + int u = (digest.HashSize >> 3); // div 8 + int v = 64; + byte[] dKey = new byte [n]; + + byte[] S; + if ((_salt != null) && (_salt.Length != 0)) { + S = new byte[v * ((_salt.Length + v - 1) / v)]; + + for (int i = 0; i != S.Length; i++) { + S[i] = _salt[i % _salt.Length]; + } + } + else { + S = new byte[0]; + } + + byte[] P; + if ((_password != null) && (_password.Length != 0)) { + P = new byte[v * ((_password.Length + v - 1) / v)]; + + for (int i = 0; i != P.Length; i++) { + P[i] = _password[i % _password.Length]; + } + } + else { + P = new byte[0]; + } + + byte[] I = new byte [S.Length + P.Length]; + + Buffer.BlockCopy (S, 0, I, 0, S.Length); + Buffer.BlockCopy (P, 0, I, S.Length, P.Length); + + byte[] B = new byte[v]; + int c = (n + u - 1) / u; + + for (int i = 1; i <= c; i++) { + digest.TransformBlock (diversifier, 0, diversifier.Length, diversifier, 0); + digest.TransformFinalBlock (I, 0, I.Length); + byte[] A = digest.Hash; + digest.Initialize (); + for (int j = 1; j != _iterations; j++) { + A = digest.ComputeHash (A, 0, A.Length); + } + + for (int j = 0; j != B.Length; j++) { + B [j] = A [j % A.Length]; + } + + for (int j = 0; j != I.Length / v; j++) { + Adjust (I, j * v, B); + } + + if (i == c) { + Buffer.BlockCopy(A, 0, dKey, (i - 1) * u, dKey.Length - ((i - 1) * u)); + } + else { + Buffer.BlockCopy(A, 0, dKey, (i - 1) * u, A.Length); + } + } + + return dKey; + } + + public byte[] DeriveKey (int size) + { + return Derive (keyDiversifier, size); + } + + public byte[] DeriveIV (int size) + { + return Derive (ivDiversifier, size); + } + + public byte[] DeriveMAC (int size) + { + return Derive (macDiversifier, size); + } + } + + const int recommendedIterationCount = 2000; + + //private int _version; + private byte[] _password; + private ArrayList _keyBags; + private ArrayList _secretBags; + private X509CertificateCollection _certs; + private bool _keyBagsChanged; + private bool _secretBagsChanged; + private bool _certsChanged; + private int _iterations; + private ArrayList _safeBags; + private RandomNumberGenerator _rng; + + // constructors + + public PKCS12 () + { + _iterations = recommendedIterationCount; + _keyBags = new ArrayList (); + _secretBags = new ArrayList (); + _certs = new X509CertificateCollection (); + _keyBagsChanged = false; + _secretBagsChanged = false; + _certsChanged = false; + _safeBags = new ArrayList (); + } + + public PKCS12 (byte[] data) + : this () + { + Password = null; + Decode (data); + } + + /* + * PFX ::= SEQUENCE { + * version INTEGER {v3(3)}(v3,...), + * authSafe ContentInfo, + * macData MacData OPTIONAL + * } + * + * MacData ::= SEQUENCE { + * mac DigestInfo, + * macSalt OCTET STRING, + * iterations INTEGER DEFAULT 1 + * -- Note: The default is for historical reasons and its use is deprecated. A higher + * -- value, like 1024 is recommended. + * } + * + * SafeContents ::= SEQUENCE OF SafeBag + * + * SafeBag ::= SEQUENCE { + * bagId BAG-TYPE.&id ({PKCS12BagSet}), + * bagValue [0] EXPLICIT BAG-TYPE.&Type({PKCS12BagSet}{@bagId}), + * bagAttributes SET OF PKCS12Attribute OPTIONAL + * } + */ + public PKCS12 (byte[] data, string password) + : this () + { + Password = password; + Decode (data); + } + + public PKCS12 (byte[] data, byte[] password) + : this () + { + _password = password; + Decode (data); + } + + private void Decode (byte[] data) + { + ASN1 pfx = new ASN1 (data); + if (pfx.Tag != 0x30) + throw new ArgumentException ("invalid data"); + + ASN1 version = pfx [0]; + if (version.Tag != 0x02) + throw new ArgumentException ("invalid PFX version"); + //_version = version.Value [0]; + + PKCS7.ContentInfo authSafe = new PKCS7.ContentInfo (pfx [1]); + if (authSafe.ContentType != PKCS7.Oid.data) + throw new ArgumentException ("invalid authenticated safe"); + + // now that we know it's a PKCS#12 file, check the (optional) MAC + // before decoding anything else in the file + if (pfx.Count > 2) { + ASN1 macData = pfx [2]; + if (macData.Tag != 0x30) + throw new ArgumentException ("invalid MAC"); + + ASN1 mac = macData [0]; + if (mac.Tag != 0x30) + throw new ArgumentException ("invalid MAC"); + ASN1 macAlgorithm = mac [0]; + string macOid = ASN1Convert.ToOid (macAlgorithm [0]); + if (macOid != "1.3.14.3.2.26") + throw new ArgumentException ("unsupported HMAC"); + byte[] macValue = mac [1].Value; + + ASN1 macSalt = macData [1]; + if (macSalt.Tag != 0x04) + throw new ArgumentException ("missing MAC salt"); + + _iterations = 1; // default value + if (macData.Count > 2) { + ASN1 iters = macData [2]; + if (iters.Tag != 0x02) + throw new ArgumentException ("invalid MAC iteration"); + _iterations = ASN1Convert.ToInt32 (iters); + } + + byte[] authSafeData = authSafe.Content [0].Value; + byte[] calculatedMac = MAC (_password, macSalt.Value, _iterations, authSafeData); + if (!Compare (macValue, calculatedMac)) { + byte[] nullPassword = {0, 0}; + calculatedMac = MAC(nullPassword, macSalt.Value, _iterations, authSafeData); + if (!Compare (macValue, calculatedMac)) + throw new CryptographicException ("Invalid MAC - file may have been tampe red!"); + _password = nullPassword; + } + } + + // we now returns to our original presentation - PFX + ASN1 authenticatedSafe = new ASN1 (authSafe.Content [0].Value); + for (int i=0; i < authenticatedSafe.Count; i++) { + PKCS7.ContentInfo ci = new PKCS7.ContentInfo (authenticatedSafe [i]); + switch (ci.ContentType) { + case PKCS7.Oid.data: + // unencrypted (by PKCS#12) + ASN1 safeContents = new ASN1 (ci.Content [0].Value); + for (int j=0; j < safeContents.Count; j++) { + ASN1 safeBag = safeContents [j]; + ReadSafeBag (safeBag); + } + break; + case PKCS7.Oid.encryptedData: + // password encrypted + PKCS7.EncryptedData ed = new PKCS7.EncryptedData (ci.Content [0]); + ASN1 decrypted = new ASN1 (Decrypt (ed)); + for (int j=0; j < decrypted.Count; j++) { + ASN1 safeBag = decrypted [j]; + ReadSafeBag (safeBag); + } + break; + case PKCS7.Oid.envelopedData: + // public key encrypted + throw new NotImplementedException ("public key encrypted"); + default: + throw new ArgumentException ("unknown authenticatedSafe"); + } + } + } + + ~PKCS12 () + { + if (_password != null) { + Array.Clear (_password, 0, _password.Length); + } + _password = null; + } + + // properties + + public string Password { + set { + // Clear old password. + if (_password != null) + Array.Clear (_password, 0, _password.Length); + _password = null; + if (value != null) { + if (value.Length > 0) { + int size = value.Length; + int nul = 0; + if (size < MaximumPasswordLength) { + // if not present, add space for a NULL (0x00) character + if (value[size - 1] != 0x00) + nul = 1; + } else { + size = MaximumPasswordLength; + } + _password = new byte[(size + nul) << 1]; // double for unicode + Encoding.BigEndianUnicode.GetBytes (value, 0, size, _password, 0); + } else { + // double-byte (Unicode) NULL (0x00) - see bug #79617 + _password = new byte[2]; + } + } + } + } + + public int IterationCount { + get { return _iterations; } + set { _iterations = value; } + } + + public ArrayList Keys { + get { + if (_keyBagsChanged) { + _keyBags.Clear (); + foreach (SafeBag sb in _safeBags) { + if (sb.BagOID.Equals (keyBag)) { + ASN1 safeBag = sb.ASN1; + ASN1 bagValue = safeBag [1]; + PKCS8.PrivateKeyInfo pki = new PKCS8.PrivateKeyInfo (bagValue.Value); + byte[] privateKey = pki.PrivateKey; + switch (privateKey [0]) { + case 0x02: + DSAParameters p = new DSAParameters (); // FIXME + _keyBags.Add (PKCS8.PrivateKeyInfo.DecodeDSA (privateKey, p)); + break; + case 0x30: + _keyBags.Add (PKCS8.PrivateKeyInfo.DecodeRSA (privateKey)); + break; + default: + break; + } + Array.Clear (privateKey, 0, privateKey.Length); + + } else if (sb.BagOID.Equals (pkcs8ShroudedKeyBag)) { + ASN1 safeBag = sb.ASN1; + ASN1 bagValue = safeBag [1]; + PKCS8.EncryptedPrivateKeyInfo epki = new PKCS8.EncryptedPrivateKeyInfo (bagValue.Value); + byte[] decrypted = Decrypt (epki.Algorithm, epki.Salt, epki.IterationCount, epki.EncryptedData); + PKCS8.PrivateKeyInfo pki = new PKCS8.PrivateKeyInfo (decrypted); + byte[] privateKey = pki.PrivateKey; + switch (privateKey [0]) { + case 0x02: + DSAParameters p = new DSAParameters (); // FIXME + _keyBags.Add (PKCS8.PrivateKeyInfo.DecodeDSA (privateKey, p)); + break; + case 0x30: + _keyBags.Add (PKCS8.PrivateKeyInfo.DecodeRSA (privateKey)); + break; + default: + break; + } + Array.Clear (privateKey, 0, privateKey.Length); + Array.Clear (decrypted, 0, decrypted.Length); + } + } + _keyBagsChanged = false; + } + return ArrayList.ReadOnly(_keyBags); + } + } + + public ArrayList Secrets { + get { + if (_secretBagsChanged) { + _secretBags.Clear (); + foreach (SafeBag sb in _safeBags) { + if (sb.BagOID.Equals (secretBag)) { + ASN1 safeBag = sb.ASN1; + ASN1 bagValue = safeBag [1]; + byte[] secret = bagValue.Value; + _secretBags.Add(secret); + } + } + _secretBagsChanged = false; + } + return ArrayList.ReadOnly(_secretBags); + } + } + + public X509CertificateCollection Certificates { + get { + if (_certsChanged) { + _certs.Clear (); + foreach (SafeBag sb in _safeBags) { + if (sb.BagOID.Equals (certBag)) { + ASN1 safeBag = sb.ASN1; + ASN1 bagValue = safeBag [1]; + PKCS7.ContentInfo cert = new PKCS7.ContentInfo (bagValue.Value); + _certs.Add (new X509Certificate (cert.Content [0].Value)); + } + } + _certsChanged = false; + } + return _certs; + } + } + + internal RandomNumberGenerator RNG { + get { + if (_rng == null) + _rng = RandomNumberGenerator.Create (); + return _rng; + } + } + + // private methods + + private bool Compare (byte[] expected, byte[] actual) + { + bool compare = false; + if (expected.Length == actual.Length) { + for (int i=0; i < expected.Length; i++) { + if (expected [i] != actual [i]) + return false; + } + compare = true; + } + return compare; + } + + private SymmetricAlgorithm GetSymmetricAlgorithm (string algorithmOid, byte[] salt, int iterationCount) + { + string algorithm = null; + int keyLength = 8; // 64 bits (default) + int ivLength = 8; // 64 bits (default) + + PKCS12.DeriveBytes pd = new PKCS12.DeriveBytes (); + pd.Password = _password; + pd.Salt = salt; + pd.IterationCount = iterationCount; + + switch (algorithmOid) { + case PKCS5.pbeWithMD2AndDESCBC: // no unit test available + pd.HashName = "MD2"; + algorithm = "DES"; + break; + case PKCS5.pbeWithMD5AndDESCBC: // no unit test available + pd.HashName = "MD5"; + algorithm = "DES"; + break; + case PKCS5.pbeWithMD2AndRC2CBC: // no unit test available + // TODO - RC2-CBC-Parameter (PKCS5) + // if missing default to 32 bits !!! + pd.HashName = "MD2"; + algorithm = "RC2"; + keyLength = 4; // default + break; + case PKCS5.pbeWithMD5AndRC2CBC: // no unit test available + // TODO - RC2-CBC-Parameter (PKCS5) + // if missing default to 32 bits !!! + pd.HashName = "MD5"; + algorithm = "RC2"; + keyLength = 4; // default + break; + case PKCS5.pbeWithSHA1AndDESCBC: // no unit test available + pd.HashName = "SHA1"; + algorithm = "DES"; + break; + case PKCS5.pbeWithSHA1AndRC2CBC: // no unit test available + // TODO - RC2-CBC-Parameter (PKCS5) + // if missing default to 32 bits !!! + pd.HashName = "SHA1"; + algorithm = "RC2"; + keyLength = 4; // default + break; + case PKCS12.pbeWithSHAAnd128BitRC4: // no unit test available + pd.HashName = "SHA1"; + algorithm = "RC4"; + keyLength = 16; + ivLength = 0; // N/A + break; + case PKCS12.pbeWithSHAAnd40BitRC4: // no unit test available + pd.HashName = "SHA1"; + algorithm = "RC4"; + keyLength = 5; + ivLength = 0; // N/A + break; + case PKCS12.pbeWithSHAAnd3KeyTripleDESCBC: + pd.HashName = "SHA1"; + algorithm = "TripleDES"; + keyLength = 24; + break; + case PKCS12.pbeWithSHAAnd2KeyTripleDESCBC: // no unit test available + pd.HashName = "SHA1"; + algorithm = "TripleDES"; + keyLength = 16; + break; + case PKCS12.pbeWithSHAAnd128BitRC2CBC: // no unit test available + pd.HashName = "SHA1"; + algorithm = "RC2"; + keyLength = 16; + break; + case PKCS12.pbeWithSHAAnd40BitRC2CBC: + pd.HashName = "SHA1"; + algorithm = "RC2"; + keyLength = 5; + break; + default: + throw new NotSupportedException ("unknown oid " + algorithm); + } + + SymmetricAlgorithm sa = null; + sa = SymmetricAlgorithm.Create(algorithm); + sa.Key = pd.DeriveKey (keyLength); + // IV required only for block ciphers (not stream ciphers) + if (ivLength > 0) { + sa.IV = pd.DeriveIV (ivLength); + sa.Mode = CipherMode.CBC; + } + return sa; + } + + public byte[] Decrypt (string algorithmOid, byte[] salt, int iterationCount, byte[] encryptedData) + { + SymmetricAlgorithm sa = null; + byte[] result = null; + try { + sa = GetSymmetricAlgorithm (algorithmOid, salt, iterationCount); + ICryptoTransform ct = sa.CreateDecryptor (); + result = ct.TransformFinalBlock (encryptedData, 0, encryptedData.Length); + } + finally { + if (sa != null) + sa.Clear (); + } + return result; + } + + public byte[] Decrypt (PKCS7.EncryptedData ed) + { + return Decrypt (ed.EncryptionAlgorithm.ContentType, + ed.EncryptionAlgorithm.Content [0].Value, + ASN1Convert.ToInt32 (ed.EncryptionAlgorithm.Content [1]), + ed.EncryptedContent); + } + + public byte[] Encrypt (string algorithmOid, byte[] salt, int iterationCount, byte[] data) + { + byte[] result = null; + using (SymmetricAlgorithm sa = GetSymmetricAlgorithm (algorithmOid, salt, iterationCount)) { + ICryptoTransform ct = sa.CreateEncryptor (); + result = ct.TransformFinalBlock (data, 0, data.Length); + } + return result; + } + + private DSAParameters GetExistingParameters (out bool found) + { + foreach (X509Certificate cert in Certificates) { + // FIXME: that won't work if parts of the parameters are missing + if (cert.KeyAlgorithmParameters != null) { + DSA dsa = cert.DSA; + if (dsa != null) { + found = true; + return dsa.ExportParameters (false); + } + } + } + found = false; + return new DSAParameters (); + } + + private void AddPrivateKey (PKCS8.PrivateKeyInfo pki) + { + byte[] privateKey = pki.PrivateKey; + switch (privateKey [0]) { + case 0x02: + bool found; + DSAParameters p = GetExistingParameters (out found); + if (found) { + _keyBags.Add (PKCS8.PrivateKeyInfo.DecodeDSA (privateKey, p)); + } + break; + case 0x30: + _keyBags.Add (PKCS8.PrivateKeyInfo.DecodeRSA (privateKey)); + break; + default: + Array.Clear (privateKey, 0, privateKey.Length); + throw new CryptographicException ("Unknown private key format"); + } + Array.Clear (privateKey, 0, privateKey.Length); + } + + private void ReadSafeBag (ASN1 safeBag) + { + if (safeBag.Tag != 0x30) + throw new ArgumentException ("invalid safeBag"); + + ASN1 bagId = safeBag [0]; + if (bagId.Tag != 0x06) + throw new ArgumentException ("invalid safeBag id"); + + ASN1 bagValue = safeBag [1]; + string oid = ASN1Convert.ToOid (bagId); + switch (oid) { + case keyBag: + // NEED UNIT TEST + AddPrivateKey (new PKCS8.PrivateKeyInfo (bagValue.Value)); + break; + case pkcs8ShroudedKeyBag: + PKCS8.EncryptedPrivateKeyInfo epki = new PKCS8.EncryptedPrivateKeyInfo (bagValue.Value); + byte[] decrypted = Decrypt (epki.Algorithm, epki.Salt, epki.IterationCount, epki.EncryptedData); + AddPrivateKey (new PKCS8.PrivateKeyInfo (decrypted)); + Array.Clear (decrypted, 0, decrypted.Length); + break; + case certBag: + PKCS7.ContentInfo cert = new PKCS7.ContentInfo (bagValue.Value); + if (cert.ContentType != x509Certificate) + throw new NotSupportedException ("unsupport certificate type"); + X509Certificate x509 = new X509Certificate (cert.Content [0].Value); + _certs.Add (x509); + break; + case crlBag: + // TODO + break; + case secretBag: + byte[] secret = bagValue.Value; + _secretBags.Add(secret); + break; + case safeContentsBag: + // TODO - ? recurse ? + break; + default: + throw new ArgumentException ("unknown safeBag oid"); + } + + if (safeBag.Count > 2) { + ASN1 bagAttributes = safeBag [2]; + if (bagAttributes.Tag != 0x31) + throw new ArgumentException ("invalid safeBag attributes id"); + + for (int i = 0; i < bagAttributes.Count; i++) { + ASN1 pkcs12Attribute = bagAttributes[i]; + + if (pkcs12Attribute.Tag != 0x30) + throw new ArgumentException ("invalid PKCS12 attributes id"); + + ASN1 attrId = pkcs12Attribute [0]; + if (attrId.Tag != 0x06) + throw new ArgumentException ("invalid attribute id"); + + string attrOid = ASN1Convert.ToOid (attrId); + + ASN1 attrValues = pkcs12Attribute[1]; + for (int j = 0; j < attrValues.Count; j++) { + ASN1 attrValue = attrValues[j]; + + switch (attrOid) { + case PKCS9.friendlyName: + if (attrValue.Tag != 0x1e) + throw new ArgumentException ("invalid attribute value id"); + break; + case PKCS9.localKeyId: + if (attrValue.Tag != 0x04) + throw new ArgumentException ("invalid attribute value id"); + break; + default: + // Unknown OID -- don't check Tag + break; + } + } + } + } + + _safeBags.Add (new SafeBag(oid, safeBag)); + } + + private ASN1 Pkcs8ShroudedKeyBagSafeBag (AsymmetricAlgorithm aa, IDictionary attributes) + { + PKCS8.PrivateKeyInfo pki = new PKCS8.PrivateKeyInfo (); + if (aa is RSA) { + pki.Algorithm = "1.2.840.113549.1.1.1"; + pki.PrivateKey = PKCS8.PrivateKeyInfo.Encode ((RSA)aa); + } + else if (aa is DSA) { + pki.Algorithm = null; + pki.PrivateKey = PKCS8.PrivateKeyInfo.Encode ((DSA)aa); + } + else + throw new CryptographicException ("Unknown asymmetric algorithm {0}", aa.ToString ()); + + PKCS8.EncryptedPrivateKeyInfo epki = new PKCS8.EncryptedPrivateKeyInfo (); + epki.Algorithm = pbeWithSHAAnd3KeyTripleDESCBC; + epki.IterationCount = _iterations; + epki.EncryptedData = Encrypt (pbeWithSHAAnd3KeyTripleDESCBC, epki.Salt, _iterations, pki.GetBytes ()); + + ASN1 safeBag = new ASN1 (0x30); + safeBag.Add (ASN1Convert.FromOid (pkcs8ShroudedKeyBag)); + ASN1 bagValue = new ASN1 (0xA0); + bagValue.Add (new ASN1 (epki.GetBytes ())); + safeBag.Add (bagValue); + + if (attributes != null) { + ASN1 bagAttributes = new ASN1 (0x31); + IDictionaryEnumerator de = attributes.GetEnumerator (); + + while (de.MoveNext ()) { + string oid = (string)de.Key; + switch (oid) { + case PKCS9.friendlyName: + ArrayList names = (ArrayList)de.Value; + if (names.Count > 0) { + ASN1 pkcs12Attribute = new ASN1 (0x30); + pkcs12Attribute.Add (ASN1Convert.FromOid (PKCS9.friendlyName)); + ASN1 attrValues = new ASN1 (0x31); + foreach (byte[] name in names) { + ASN1 attrValue = new ASN1 (0x1e); + attrValue.Value = name; + attrValues.Add (attrValue); + } + pkcs12Attribute.Add (attrValues); + bagAttributes.Add (pkcs12Attribute); + } + break; + case PKCS9.localKeyId: + ArrayList keys = (ArrayList)de.Value; + if (keys.Count > 0) { + ASN1 pkcs12Attribute = new ASN1 (0x30); + pkcs12Attribute.Add (ASN1Convert.FromOid (PKCS9.localKeyId)); + ASN1 attrValues = new ASN1 (0x31); + foreach (byte[] key in keys) { + ASN1 attrValue = new ASN1 (0x04); + attrValue.Value = key; + attrValues.Add (attrValue); + } + pkcs12Attribute.Add (attrValues); + bagAttributes.Add (pkcs12Attribute); + } + break; + default: + break; + } + } + + if (bagAttributes.Count > 0) { + safeBag.Add (bagAttributes); + } + } + + return safeBag; + } + + private ASN1 KeyBagSafeBag (AsymmetricAlgorithm aa, IDictionary attributes) + { + PKCS8.PrivateKeyInfo pki = new PKCS8.PrivateKeyInfo (); + if (aa is RSA) { + pki.Algorithm = "1.2.840.113549.1.1.1"; + pki.PrivateKey = PKCS8.PrivateKeyInfo.Encode ((RSA)aa); + } + else if (aa is DSA) { + pki.Algorithm = null; + pki.PrivateKey = PKCS8.PrivateKeyInfo.Encode ((DSA)aa); + } + else + throw new CryptographicException ("Unknown asymmetric algorithm {0}", aa.ToString ()); + + ASN1 safeBag = new ASN1 (0x30); + safeBag.Add (ASN1Convert.FromOid (keyBag)); + ASN1 bagValue = new ASN1 (0xA0); + bagValue.Add (new ASN1 (pki.GetBytes ())); + safeBag.Add (bagValue); + + if (attributes != null) { + ASN1 bagAttributes = new ASN1 (0x31); + IDictionaryEnumerator de = attributes.GetEnumerator (); + + while (de.MoveNext ()) { + string oid = (string)de.Key; + switch (oid) { + case PKCS9.friendlyName: + ArrayList names = (ArrayList)de.Value; + if (names.Count > 0) { + ASN1 pkcs12Attribute = new ASN1 (0x30); + pkcs12Attribute.Add (ASN1Convert.FromOid (PKCS9.friendlyName)); + ASN1 attrValues = new ASN1 (0x31); + foreach (byte[] name in names) { + ASN1 attrValue = new ASN1 (0x1e); + attrValue.Value = name; + attrValues.Add (attrValue); + } + pkcs12Attribute.Add (attrValues); + bagAttributes.Add (pkcs12Attribute); + } + break; + case PKCS9.localKeyId: + ArrayList keys = (ArrayList)de.Value; + if (keys.Count > 0) { + ASN1 pkcs12Attribute = new ASN1 (0x30); + pkcs12Attribute.Add (ASN1Convert.FromOid (PKCS9.localKeyId)); + ASN1 attrValues = new ASN1 (0x31); + foreach (byte[] key in keys) { + ASN1 attrValue = new ASN1 (0x04); + attrValue.Value = key; + attrValues.Add (attrValue); + } + pkcs12Attribute.Add (attrValues); + bagAttributes.Add (pkcs12Attribute); + } + break; + default: + break; + } + } + + if (bagAttributes.Count > 0) { + safeBag.Add (bagAttributes); + } + } + + return safeBag; + } + + private ASN1 SecretBagSafeBag (byte[] secret, IDictionary attributes) + { + ASN1 safeBag = new ASN1 (0x30); + safeBag.Add (ASN1Convert.FromOid (secretBag)); + ASN1 bagValue = new ASN1 (0x80, secret); + safeBag.Add (bagValue); + + if (attributes != null) { + ASN1 bagAttributes = new ASN1 (0x31); + IDictionaryEnumerator de = attributes.GetEnumerator (); + + while (de.MoveNext ()) { + string oid = (string)de.Key; + switch (oid) { + case PKCS9.friendlyName: + ArrayList names = (ArrayList)de.Value; + if (names.Count > 0) { + ASN1 pkcs12Attribute = new ASN1 (0x30); + pkcs12Attribute.Add (ASN1Convert.FromOid (PKCS9.friendlyName)); + ASN1 attrValues = new ASN1 (0x31); + foreach (byte[] name in names) { + ASN1 attrValue = new ASN1 (0x1e); + attrValue.Value = name; + attrValues.Add (attrValue); + } + pkcs12Attribute.Add (attrValues); + bagAttributes.Add (pkcs12Attribute); + } + break; + case PKCS9.localKeyId: + ArrayList keys = (ArrayList)de.Value; + if (keys.Count > 0) { + ASN1 pkcs12Attribute = new ASN1 (0x30); + pkcs12Attribute.Add (ASN1Convert.FromOid (PKCS9.localKeyId)); + ASN1 attrValues = new ASN1 (0x31); + foreach (byte[] key in keys) { + ASN1 attrValue = new ASN1 (0x04); + attrValue.Value = key; + attrValues.Add (attrValue); + } + pkcs12Attribute.Add (attrValues); + bagAttributes.Add (pkcs12Attribute); + } + break; + default: + break; + } + } + + if (bagAttributes.Count > 0) { + safeBag.Add (bagAttributes); + } + } + + return safeBag; + } + + private ASN1 CertificateSafeBag (X509Certificate x509, IDictionary attributes) + { + ASN1 encapsulatedCertificate = new ASN1 (0x04, x509.RawData); + + PKCS7.ContentInfo ci = new PKCS7.ContentInfo (); + ci.ContentType = x509Certificate; + ci.Content.Add (encapsulatedCertificate); + + ASN1 bagValue = new ASN1 (0xA0); + bagValue.Add (ci.ASN1); + + ASN1 safeBag = new ASN1 (0x30); + safeBag.Add (ASN1Convert.FromOid (certBag)); + safeBag.Add (bagValue); + + if (attributes != null) { + ASN1 bagAttributes = new ASN1 (0x31); + IDictionaryEnumerator de = attributes.GetEnumerator (); + + while (de.MoveNext ()) { + string oid = (string)de.Key; + switch (oid) { + case PKCS9.friendlyName: + ArrayList names = (ArrayList)de.Value; + if (names.Count > 0) { + ASN1 pkcs12Attribute = new ASN1 (0x30); + pkcs12Attribute.Add (ASN1Convert.FromOid (PKCS9.friendlyName)); + ASN1 attrValues = new ASN1 (0x31); + foreach (byte[] name in names) { + ASN1 attrValue = new ASN1 (0x1e); + attrValue.Value = name; + attrValues.Add (attrValue); + } + pkcs12Attribute.Add (attrValues); + bagAttributes.Add (pkcs12Attribute); + } + break; + case PKCS9.localKeyId: + ArrayList keys = (ArrayList)de.Value; + if (keys.Count > 0) { + ASN1 pkcs12Attribute = new ASN1 (0x30); + pkcs12Attribute.Add (ASN1Convert.FromOid (PKCS9.localKeyId)); + ASN1 attrValues = new ASN1 (0x31); + foreach (byte[] key in keys) { + ASN1 attrValue = new ASN1 (0x04); + attrValue.Value = key; + attrValues.Add (attrValue); + } + pkcs12Attribute.Add (attrValues); + bagAttributes.Add (pkcs12Attribute); + } + break; + default: + break; + } + } + + if (bagAttributes.Count > 0) { + safeBag.Add (bagAttributes); + } + } + + return safeBag; + } + + private byte[] MAC (byte[] password, byte[] salt, int iterations, byte[] data) + { + PKCS12.DeriveBytes pd = new PKCS12.DeriveBytes (); + pd.HashName = "SHA1"; + pd.Password = password; + pd.Salt = salt; + pd.IterationCount = iterations; + + HMACSHA1 hmac = (HMACSHA1) HMACSHA1.Create (); + hmac.Key = pd.DeriveMAC (20); + return hmac.ComputeHash (data, 0, data.Length); + } + + /* + * SafeContents ::= SEQUENCE OF SafeBag + * + * SafeBag ::= SEQUENCE { + * bagId BAG-TYPE.&id ({PKCS12BagSet}), + * bagValue [0] EXPLICIT BAG-TYPE.&Type({PKCS12BagSet}{@bagId}), + * bagAttributes SET OF PKCS12Attribute OPTIONAL + * } + */ + public byte[] GetBytes () + { + // TODO (incomplete) + ASN1 safeBagSequence = new ASN1 (0x30); + + // Sync Safe Bag list since X509CertificateCollection may be updated + ArrayList scs = new ArrayList (); + foreach (SafeBag sb in _safeBags) { + if (sb.BagOID.Equals (certBag)) { + ASN1 safeBag = sb.ASN1; + ASN1 bagValue = safeBag [1]; + PKCS7.ContentInfo cert = new PKCS7.ContentInfo (bagValue.Value); + scs.Add (new X509Certificate (cert.Content [0].Value)); + } + } + + ArrayList addcerts = new ArrayList (); + ArrayList removecerts = new ArrayList (); + + foreach (X509Certificate c in Certificates) { + bool found = false; + + foreach (X509Certificate lc in scs) { + if (Compare (c.RawData, lc.RawData)) { + found = true; + } + } + + if (!found) { + addcerts.Add (c); + } + } + foreach (X509Certificate c in scs) { + bool found = false; + + foreach (X509Certificate lc in Certificates) { + if (Compare (c.RawData, lc.RawData)) { + found = true; + } + } + + if (!found) { + removecerts.Add (c); + } + } + + foreach (X509Certificate c in removecerts) { + RemoveCertificate (c); + } + + foreach (X509Certificate c in addcerts) { + AddCertificate (c); + } + // Sync done + + if (_safeBags.Count > 0) { + ASN1 certsSafeBag = new ASN1 (0x30); + foreach (SafeBag sb in _safeBags) { + if (sb.BagOID.Equals (certBag)) { + certsSafeBag.Add (sb.ASN1); + } + } + + if (certsSafeBag.Count > 0) { + PKCS7.ContentInfo contentInfo = EncryptedContentInfo (certsSafeBag, pbeWithSHAAnd3KeyTripleDESCBC); + safeBagSequence.Add (contentInfo.ASN1); + } + } + + if (_safeBags.Count > 0) { + ASN1 safeContents = new ASN1 (0x30); + foreach (SafeBag sb in _safeBags) { + if (sb.BagOID.Equals (keyBag) || + sb.BagOID.Equals (pkcs8ShroudedKeyBag)) { + safeContents.Add (sb.ASN1); + } + } + if (safeContents.Count > 0) { + ASN1 content = new ASN1 (0xA0); + content.Add (new ASN1 (0x04, safeContents.GetBytes ())); + + PKCS7.ContentInfo keyBag = new PKCS7.ContentInfo (PKCS7.Oid.data); + keyBag.Content = content; + safeBagSequence.Add (keyBag.ASN1); + } + } + + // Doing SecretBags separately in case we want to change their encryption independently. + if (_safeBags.Count > 0) { + ASN1 secretsSafeBag = new ASN1 (0x30); + foreach (SafeBag sb in _safeBags) { + if (sb.BagOID.Equals (secretBag)) { + secretsSafeBag.Add (sb.ASN1); + } + } + + if (secretsSafeBag.Count > 0) { + PKCS7.ContentInfo contentInfo = EncryptedContentInfo (secretsSafeBag, pbeWithSHAAnd3KeyTripleDESCBC); + safeBagSequence.Add (contentInfo.ASN1); + } + } + + + ASN1 encapsulates = new ASN1 (0x04, safeBagSequence.GetBytes ()); + ASN1 ci = new ASN1 (0xA0); + ci.Add (encapsulates); + PKCS7.ContentInfo authSafe = new PKCS7.ContentInfo (PKCS7.Oid.data); + authSafe.Content = ci; + + ASN1 macData = new ASN1 (0x30); + if (_password != null) { + // only for password based encryption + byte[] salt = new byte [20]; + RNG.GetBytes (salt); + byte[] macValue = MAC (_password, salt, _iterations, authSafe.Content [0].Value); + ASN1 oidSeq = new ASN1 (0x30); + oidSeq.Add (ASN1Convert.FromOid ("1.3.14.3.2.26")); // SHA1 + oidSeq.Add (new ASN1 (0x05)); + + ASN1 mac = new ASN1 (0x30); + mac.Add (oidSeq); + mac.Add (new ASN1 (0x04, macValue)); + + macData.Add (mac); + macData.Add (new ASN1 (0x04, salt)); + macData.Add (ASN1Convert.FromInt32 (_iterations)); + } + + ASN1 version = new ASN1 (0x02, new byte [1] { 0x03 }); + + ASN1 pfx = new ASN1 (0x30); + pfx.Add (version); + pfx.Add (authSafe.ASN1); + if (macData.Count > 0) { + // only for password based encryption + pfx.Add (macData); + } + + return pfx.GetBytes (); + } + + // Creates an encrypted PKCS#7 ContentInfo with safeBags as its SafeContents. Used in GetBytes(), above. + private PKCS7.ContentInfo EncryptedContentInfo(ASN1 safeBags, string algorithmOid) + { + byte[] salt = new byte [8]; + RNG.GetBytes (salt); + + ASN1 seqParams = new ASN1 (0x30); + seqParams.Add (new ASN1 (0x04, salt)); + seqParams.Add (ASN1Convert.FromInt32 (_iterations)); + + ASN1 seqPbe = new ASN1 (0x30); + seqPbe.Add (ASN1Convert.FromOid (algorithmOid)); + seqPbe.Add (seqParams); + + byte[] encrypted = Encrypt (algorithmOid, salt, _iterations, safeBags.GetBytes ()); + ASN1 encryptedContent = new ASN1 (0x80, encrypted); + + ASN1 seq = new ASN1 (0x30); + seq.Add (ASN1Convert.FromOid (PKCS7.Oid.data)); + seq.Add (seqPbe); + seq.Add (encryptedContent); + + ASN1 version = new ASN1 (0x02, new byte [1] { 0x00 }); + ASN1 encData = new ASN1 (0x30); + encData.Add (version); + encData.Add (seq); + + ASN1 finalContent = new ASN1 (0xA0); + finalContent.Add (encData); + + PKCS7.ContentInfo bag = new PKCS7.ContentInfo (PKCS7.Oid.encryptedData); + bag.Content = finalContent; + return bag; + } + + public void AddCertificate (X509Certificate cert) + { + AddCertificate (cert, null); + } + + public void AddCertificate (X509Certificate cert, IDictionary attributes) + { + bool found = false; + + for (int i = 0; !found && i < _safeBags.Count; i++) { + SafeBag sb = (SafeBag)_safeBags [i]; + + if (sb.BagOID.Equals (certBag)) { + ASN1 safeBag = sb.ASN1; + ASN1 bagValue = safeBag [1]; + PKCS7.ContentInfo crt = new PKCS7.ContentInfo (bagValue.Value); + X509Certificate c = new X509Certificate (crt.Content [0].Value); + if (Compare (cert.RawData, c.RawData)) { + found = true; + } + } + } + + if (!found) { + _safeBags.Add (new SafeBag (certBag, CertificateSafeBag (cert, attributes))); + _certsChanged = true; + } + } + + public void RemoveCertificate (X509Certificate cert) + { + RemoveCertificate (cert, null); + } + + public void RemoveCertificate (X509Certificate cert, IDictionary attrs) + { + int certIndex = -1; + + for (int i = 0; certIndex == -1 && i < _safeBags.Count; i++) { + SafeBag sb = (SafeBag)_safeBags [i]; + + if (sb.BagOID.Equals (certBag)) { + ASN1 safeBag = sb.ASN1; + ASN1 bagValue = safeBag [1]; + PKCS7.ContentInfo crt = new PKCS7.ContentInfo (bagValue.Value); + X509Certificate c = new X509Certificate (crt.Content [0].Value); + if (Compare (cert.RawData, c.RawData)) { + if (attrs != null) { + if (safeBag.Count == 3) { + ASN1 bagAttributes = safeBag [2]; + int bagAttributesFound = 0; + for (int j = 0; j < bagAttributes.Count; j++) { + ASN1 pkcs12Attribute = bagAttributes [j]; + ASN1 attrId = pkcs12Attribute [0]; + string ao = ASN1Convert.ToOid (attrId); + ArrayList dattrValues = (ArrayList)attrs [ao]; + + if (dattrValues != null) { + ASN1 attrValues = pkcs12Attribute [1]; + + if (dattrValues.Count == attrValues.Count) { + int attrValuesFound = 0; + for (int k = 0; k < attrValues.Count; k++) { + ASN1 attrValue = attrValues [k]; + byte[] value = (byte[])dattrValues [k]; + + if (Compare (value, attrValue.Value)) { + attrValuesFound += 1; + } + } + if (attrValuesFound == attrValues.Count) { + bagAttributesFound += 1; + } + } + } + } + if (bagAttributesFound == bagAttributes.Count) { + certIndex = i; + } + } + } else { + certIndex = i; + } + } + } + } + + if (certIndex != -1) { + _safeBags.RemoveAt (certIndex); + _certsChanged = true; + } + } + + private bool CompareAsymmetricAlgorithm (AsymmetricAlgorithm a1, AsymmetricAlgorithm a2) + { + // fast path + if (a1.KeySize != a2.KeySize) + return false; + // compare public keys - if they match we can assume the private match too + return (a1.ToXmlString (false) == a2.ToXmlString (false)); + } + + public void AddPkcs8ShroudedKeyBag (AsymmetricAlgorithm aa) + { + AddPkcs8ShroudedKeyBag (aa, null); + } + + public void AddPkcs8ShroudedKeyBag (AsymmetricAlgorithm aa, IDictionary attributes) + { + bool found = false; + + for (int i = 0; !found && i < _safeBags.Count; i++) { + SafeBag sb = (SafeBag)_safeBags [i]; + + if (sb.BagOID.Equals (pkcs8ShroudedKeyBag)) { + ASN1 bagValue = sb.ASN1 [1]; + PKCS8.EncryptedPrivateKeyInfo epki = new PKCS8.EncryptedPrivateKeyInfo (bagValue.Value); + byte[] decrypted = Decrypt (epki.Algorithm, epki.Salt, epki.IterationCount, epki.EncryptedData); + PKCS8.PrivateKeyInfo pki = new PKCS8.PrivateKeyInfo (decrypted); + byte[] privateKey = pki.PrivateKey; + + AsymmetricAlgorithm saa = null; + switch (privateKey [0]) { + case 0x02: + DSAParameters p = new DSAParameters (); // FIXME + saa = PKCS8.PrivateKeyInfo.DecodeDSA (privateKey, p); + break; + case 0x30: + saa = PKCS8.PrivateKeyInfo.DecodeRSA (privateKey); + break; + default: + Array.Clear (decrypted, 0, decrypted.Length); + Array.Clear (privateKey, 0, privateKey.Length); + throw new CryptographicException ("Unknown private key format"); + } + + Array.Clear (decrypted, 0, decrypted.Length); + Array.Clear (privateKey, 0, privateKey.Length); + + if (CompareAsymmetricAlgorithm (aa , saa)) { + found = true; + } + } + } + + if (!found) { + _safeBags.Add (new SafeBag (pkcs8ShroudedKeyBag, Pkcs8ShroudedKeyBagSafeBag (aa, attributes))); + _keyBagsChanged = true; + } + } + + public void RemovePkcs8ShroudedKeyBag (AsymmetricAlgorithm aa) + { + int aaIndex = -1; + + for (int i = 0; aaIndex == -1 && i < _safeBags.Count; i++) { + SafeBag sb = (SafeBag)_safeBags [i]; + + if (sb.BagOID.Equals (pkcs8ShroudedKeyBag)) { + ASN1 bagValue = sb.ASN1 [1]; + PKCS8.EncryptedPrivateKeyInfo epki = new PKCS8.EncryptedPrivateKeyInfo (bagValue.Value); + byte[] decrypted = Decrypt (epki.Algorithm, epki.Salt, epki.IterationCount, epki.EncryptedData); + PKCS8.PrivateKeyInfo pki = new PKCS8.PrivateKeyInfo (decrypted); + byte[] privateKey = pki.PrivateKey; + + AsymmetricAlgorithm saa = null; + switch (privateKey [0]) { + case 0x02: + DSAParameters p = new DSAParameters (); // FIXME + saa = PKCS8.PrivateKeyInfo.DecodeDSA (privateKey, p); + break; + case 0x30: + saa = PKCS8.PrivateKeyInfo.DecodeRSA (privateKey); + break; + default: + Array.Clear (decrypted, 0, decrypted.Length); + Array.Clear (privateKey, 0, privateKey.Length); + throw new CryptographicException ("Unknown private key format"); + } + + Array.Clear (decrypted, 0, decrypted.Length); + Array.Clear (privateKey, 0, privateKey.Length); + + if (CompareAsymmetricAlgorithm (aa, saa)) { + aaIndex = i; + } + } + } + + if (aaIndex != -1) { + _safeBags.RemoveAt (aaIndex); + _keyBagsChanged = true; + } + } + + public void AddKeyBag (AsymmetricAlgorithm aa) + { + AddKeyBag (aa, null); + } + + public void AddKeyBag (AsymmetricAlgorithm aa, IDictionary attributes) + { + bool found = false; + + for (int i = 0; !found && i < _safeBags.Count; i++) { + SafeBag sb = (SafeBag)_safeBags [i]; + + if (sb.BagOID.Equals (keyBag)) { + ASN1 bagValue = sb.ASN1 [1]; + PKCS8.PrivateKeyInfo pki = new PKCS8.PrivateKeyInfo (bagValue.Value); + byte[] privateKey = pki.PrivateKey; + + AsymmetricAlgorithm saa = null; + switch (privateKey [0]) { + case 0x02: + DSAParameters p = new DSAParameters (); // FIXME + saa = PKCS8.PrivateKeyInfo.DecodeDSA (privateKey, p); + break; + case 0x30: + saa = PKCS8.PrivateKeyInfo.DecodeRSA (privateKey); + break; + default: + Array.Clear (privateKey, 0, privateKey.Length); + throw new CryptographicException ("Unknown private key format"); + } + + Array.Clear (privateKey, 0, privateKey.Length); + + if (CompareAsymmetricAlgorithm (aa, saa)) { + found = true; + } + } + } + + if (!found) { + _safeBags.Add (new SafeBag (keyBag, KeyBagSafeBag (aa, attributes))); + _keyBagsChanged = true; + } + } + + public void RemoveKeyBag (AsymmetricAlgorithm aa) + { + int aaIndex = -1; + + for (int i = 0; aaIndex == -1 && i < _safeBags.Count; i++) { + SafeBag sb = (SafeBag)_safeBags [i]; + + if (sb.BagOID.Equals (keyBag)) { + ASN1 bagValue = sb.ASN1 [1]; + PKCS8.PrivateKeyInfo pki = new PKCS8.PrivateKeyInfo (bagValue.Value); + byte[] privateKey = pki.PrivateKey; + + AsymmetricAlgorithm saa = null; + switch (privateKey [0]) { + case 0x02: + DSAParameters p = new DSAParameters (); // FIXME + saa = PKCS8.PrivateKeyInfo.DecodeDSA (privateKey, p); + break; + case 0x30: + saa = PKCS8.PrivateKeyInfo.DecodeRSA (privateKey); + break; + default: + Array.Clear (privateKey, 0, privateKey.Length); + throw new CryptographicException ("Unknown private key format"); + } + + Array.Clear (privateKey, 0, privateKey.Length); + + if (CompareAsymmetricAlgorithm (aa, saa)) { + aaIndex = i; + } + } + } + + if (aaIndex != -1) { + _safeBags.RemoveAt (aaIndex); + _keyBagsChanged = true; + } + } + + public void AddSecretBag (byte[] secret) + { + AddSecretBag (secret, null); + } + + public void AddSecretBag (byte[] secret, IDictionary attributes) + { + bool found = false; + + for (int i = 0; !found && i < _safeBags.Count; i++) { + SafeBag sb = (SafeBag)_safeBags [i]; + + if (sb.BagOID.Equals (secretBag)) { + ASN1 bagValue = sb.ASN1 [1]; + byte[] ssecret = bagValue.Value; + + if (Compare (secret, ssecret)) { + found = true; + } + } + } + + if (!found) { + _safeBags.Add (new SafeBag (secretBag, SecretBagSafeBag (secret, attributes))); + _secretBagsChanged = true; + } + } + + public void RemoveSecretBag (byte[] secret) + { + int sIndex = -1; + + for (int i = 0; sIndex == -1 && i < _safeBags.Count; i++) { + SafeBag sb = (SafeBag)_safeBags [i]; + + if (sb.BagOID.Equals (secretBag)) { + ASN1 bagValue = sb.ASN1 [1]; + byte[] ssecret = bagValue.Value; + + if (Compare (secret, ssecret)) { + sIndex = i; + } + } + } + + if (sIndex != -1) { + _safeBags.RemoveAt (sIndex); + _secretBagsChanged = true; + } + } + + public AsymmetricAlgorithm GetAsymmetricAlgorithm (IDictionary attrs) + { + foreach (SafeBag sb in _safeBags) { + if (sb.BagOID.Equals (keyBag) || sb.BagOID.Equals (pkcs8ShroudedKeyBag)) { + ASN1 safeBag = sb.ASN1; + + if (safeBag.Count == 3) { + ASN1 bagAttributes = safeBag [2]; + + int bagAttributesFound = 0; + for (int i = 0; i < bagAttributes.Count; i++) { + ASN1 pkcs12Attribute = bagAttributes [i]; + ASN1 attrId = pkcs12Attribute [0]; + string ao = ASN1Convert.ToOid (attrId); + ArrayList dattrValues = (ArrayList)attrs [ao]; + + if (dattrValues != null) { + ASN1 attrValues = pkcs12Attribute [1]; + + if (dattrValues.Count == attrValues.Count) { + int attrValuesFound = 0; + for (int j = 0; j < attrValues.Count; j++) { + ASN1 attrValue = attrValues [j]; + byte[] value = (byte[])dattrValues [j]; + + if (Compare (value, attrValue.Value)) { + attrValuesFound += 1; + } + } + if (attrValuesFound == attrValues.Count) { + bagAttributesFound += 1; + } + } + } + } + if (bagAttributesFound == bagAttributes.Count) { + ASN1 bagValue = safeBag [1]; + AsymmetricAlgorithm aa = null; + if (sb.BagOID.Equals (keyBag)) { + PKCS8.PrivateKeyInfo pki = new PKCS8.PrivateKeyInfo (bagValue.Value); + byte[] privateKey = pki.PrivateKey; + switch (privateKey [0]) { + case 0x02: + DSAParameters p = new DSAParameters (); // FIXME + aa = PKCS8.PrivateKeyInfo.DecodeDSA (privateKey, p); + break; + case 0x30: + aa = PKCS8.PrivateKeyInfo.DecodeRSA (privateKey); + break; + default: + break; + } + Array.Clear (privateKey, 0, privateKey.Length); + } else if (sb.BagOID.Equals (pkcs8ShroudedKeyBag)) { + PKCS8.EncryptedPrivateKeyInfo epki = new PKCS8.EncryptedPrivateKeyInfo (bagValue.Value); + byte[] decrypted = Decrypt (epki.Algorithm, epki.Salt, epki.IterationCount, epki.EncryptedData); + PKCS8.PrivateKeyInfo pki = new PKCS8.PrivateKeyInfo (decrypted); + byte[] privateKey = pki.PrivateKey; + switch (privateKey [0]) { + case 0x02: + DSAParameters p = new DSAParameters (); // FIXME + aa = PKCS8.PrivateKeyInfo.DecodeDSA (privateKey, p); + break; + case 0x30: + aa = PKCS8.PrivateKeyInfo.DecodeRSA (privateKey); + break; + default: + break; + } + Array.Clear (privateKey, 0, privateKey.Length); + Array.Clear (decrypted, 0, decrypted.Length); + } + return aa; + } + } + } + } + + return null; + } + + public byte[] GetSecret (IDictionary attrs) + { + foreach (SafeBag sb in _safeBags) { + if (sb.BagOID.Equals (secretBag)) { + ASN1 safeBag = sb.ASN1; + + if (safeBag.Count == 3) { + ASN1 bagAttributes = safeBag [2]; + + int bagAttributesFound = 0; + for (int i = 0; i < bagAttributes.Count; i++) { + ASN1 pkcs12Attribute = bagAttributes [i]; + ASN1 attrId = pkcs12Attribute [0]; + string ao = ASN1Convert.ToOid (attrId); + ArrayList dattrValues = (ArrayList)attrs [ao]; + + if (dattrValues != null) { + ASN1 attrValues = pkcs12Attribute [1]; + + if (dattrValues.Count == attrValues.Count) { + int attrValuesFound = 0; + for (int j = 0; j < attrValues.Count; j++) { + ASN1 attrValue = attrValues [j]; + byte[] value = (byte[])dattrValues [j]; + + if (Compare (value, attrValue.Value)) { + attrValuesFound += 1; + } + } + if (attrValuesFound == attrValues.Count) { + bagAttributesFound += 1; + } + } + } + } + if (bagAttributesFound == bagAttributes.Count) { + ASN1 bagValue = safeBag [1]; + return bagValue.Value; + } + } + } + } + + return null; + } + + public X509Certificate GetCertificate (IDictionary attrs) + { + foreach (SafeBag sb in _safeBags) { + if (sb.BagOID.Equals (certBag)) { + ASN1 safeBag = sb.ASN1; + + if (safeBag.Count == 3) { + ASN1 bagAttributes = safeBag [2]; + + int bagAttributesFound = 0; + for (int i = 0; i < bagAttributes.Count; i++) { + ASN1 pkcs12Attribute = bagAttributes [i]; + ASN1 attrId = pkcs12Attribute [0]; + string ao = ASN1Convert.ToOid (attrId); + ArrayList dattrValues = (ArrayList)attrs [ao]; + + if (dattrValues != null) { + ASN1 attrValues = pkcs12Attribute [1]; + + if (dattrValues.Count == attrValues.Count) { + int attrValuesFound = 0; + for (int j = 0; j < attrValues.Count; j++) { + ASN1 attrValue = attrValues [j]; + byte[] value = (byte[])dattrValues [j]; + + if (Compare (value, attrValue.Value)) { + attrValuesFound += 1; + } + } + if (attrValuesFound == attrValues.Count) { + bagAttributesFound += 1; + } + } + } + } + if (bagAttributesFound == bagAttributes.Count) { + ASN1 bagValue = safeBag [1]; + PKCS7.ContentInfo crt = new PKCS7.ContentInfo (bagValue.Value); + return new X509Certificate (crt.Content [0].Value); + } + } + } + } + + return null; + } + + public IDictionary GetAttributes (AsymmetricAlgorithm aa) + { + IDictionary result = new Hashtable (); + + foreach (SafeBag sb in _safeBags) { + if (sb.BagOID.Equals (keyBag) || sb.BagOID.Equals (pkcs8ShroudedKeyBag)) { + ASN1 safeBag = sb.ASN1; + + ASN1 bagValue = safeBag [1]; + AsymmetricAlgorithm saa = null; + if (sb.BagOID.Equals (keyBag)) { + PKCS8.PrivateKeyInfo pki = new PKCS8.PrivateKeyInfo (bagValue.Value); + byte[] privateKey = pki.PrivateKey; + switch (privateKey [0]) { + case 0x02: + DSAParameters p = new DSAParameters (); // FIXME + saa = PKCS8.PrivateKeyInfo.DecodeDSA (privateKey, p); + break; + case 0x30: + saa = PKCS8.PrivateKeyInfo.DecodeRSA (privateKey); + break; + default: + break; + } + Array.Clear (privateKey, 0, privateKey.Length); + } else if (sb.BagOID.Equals (pkcs8ShroudedKeyBag)) { + PKCS8.EncryptedPrivateKeyInfo epki = new PKCS8.EncryptedPrivateKeyInfo (bagValue.Value); + byte[] decrypted = Decrypt (epki.Algorithm, epki.Salt, epki.IterationCount, epki.EncryptedData); + PKCS8.PrivateKeyInfo pki = new PKCS8.PrivateKeyInfo (decrypted); + byte[] privateKey = pki.PrivateKey; + switch (privateKey [0]) { + case 0x02: + DSAParameters p = new DSAParameters (); // FIXME + saa = PKCS8.PrivateKeyInfo.DecodeDSA (privateKey, p); + break; + case 0x30: + saa = PKCS8.PrivateKeyInfo.DecodeRSA (privateKey); + break; + default: + break; + } + Array.Clear (privateKey, 0, privateKey.Length); + Array.Clear (decrypted, 0, decrypted.Length); + } + + if (saa != null && CompareAsymmetricAlgorithm (saa, aa)) { + if (safeBag.Count == 3) { + ASN1 bagAttributes = safeBag [2]; + + for (int i = 0; i < bagAttributes.Count; i++) { + ASN1 pkcs12Attribute = bagAttributes [i]; + ASN1 attrId = pkcs12Attribute [0]; + string aOid = ASN1Convert.ToOid (attrId); + ArrayList aValues = new ArrayList (); + + ASN1 attrValues = pkcs12Attribute [1]; + + for (int j = 0; j < attrValues.Count; j++) { + ASN1 attrValue = attrValues [j]; + aValues.Add (attrValue.Value); + } + result.Add (aOid, aValues); + } + } + } + } + } + + return result; + } + + public IDictionary GetAttributes (X509Certificate cert) + { + IDictionary result = new Hashtable (); + + foreach (SafeBag sb in _safeBags) { + if (sb.BagOID.Equals (certBag)) { + ASN1 safeBag = sb.ASN1; + ASN1 bagValue = safeBag [1]; + PKCS7.ContentInfo crt = new PKCS7.ContentInfo (bagValue.Value); + X509Certificate xc = new X509Certificate (crt.Content [0].Value); + + if (Compare (cert.RawData, xc.RawData)) { + if (safeBag.Count == 3) { + ASN1 bagAttributes = safeBag [2]; + + for (int i = 0; i < bagAttributes.Count; i++) { + ASN1 pkcs12Attribute = bagAttributes [i]; + ASN1 attrId = pkcs12Attribute [0]; + + string aOid = ASN1Convert.ToOid (attrId); + ArrayList aValues = new ArrayList (); + + ASN1 attrValues = pkcs12Attribute [1]; + + for (int j = 0; j < attrValues.Count; j++) { + ASN1 attrValue = attrValues [j]; + aValues.Add (attrValue.Value); + } + result.Add (aOid, aValues); + } + } + } + } + } + + return result; + } + + public void SaveToFile (string filename) + { + if (filename == null) + throw new ArgumentNullException ("filename"); + + using (FileStream fs = File.Create (filename)) { + byte[] data = GetBytes (); + fs.Write (data, 0, data.Length); + } + } + + public object Clone () + { + PKCS12 clone = null; + if (_password != null) { + clone = new PKCS12 (GetBytes (), Encoding.BigEndianUnicode.GetString (_password)); + } else { + clone = new PKCS12 (GetBytes ()); + } + clone.IterationCount = this.IterationCount; + + return clone; + } + + // static + + public const int CryptoApiPasswordLimit = 32; + + static private int password_max_length = Int32.MaxValue; + + // static properties + + // MS CryptoAPI limits the password to a maximum of 31 characters + // other implementations, like OpenSSL, have no such limitation. + // Setting a maximum value will truncate the password length to + // ensure compatibility with MS's PFXImportCertStore API. + static public int MaximumPasswordLength { + get { return password_max_length; } + set { + if (value < CryptoApiPasswordLimit) { + string msg = string.Format ("Maximum password length cannot be less than {0}.", CryptoApiPasswordLimit); + throw new ArgumentOutOfRangeException (msg); + } + password_max_length = value; + } + } + } +} diff --git a/Emby.Server.Implementations/Cryptography/PKCS7.cs b/Emby.Server.Implementations/Cryptography/PKCS7.cs new file mode 100644 index 000000000..475854500 --- /dev/null +++ b/Emby.Server.Implementations/Cryptography/PKCS7.cs @@ -0,0 +1,1012 @@ +// +// PKCS7.cs: PKCS #7 - Cryptographic Message Syntax Standard +// http://www.rsasecurity.com/rsalabs/pkcs/pkcs-7/index.html +// +// Authors: +// Sebastien Pouliot <sebastien@ximian.com> +// Daniel Granath <dgranath#gmail.com> +// +// (C) 2002, 2003 Motus Technologies Inc. (http://www.motus.com) +// Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections; +using System.Security.Cryptography; + +namespace Emby.Server.Core.Cryptography +{ + + public sealed class PKCS7 { + + public class Oid { + // pkcs 1 + public const string rsaEncryption = "1.2.840.113549.1.1.1"; + // pkcs 7 + public const string data = "1.2.840.113549.1.7.1"; + public const string signedData = "1.2.840.113549.1.7.2"; + public const string envelopedData = "1.2.840.113549.1.7.3"; + public const string signedAndEnvelopedData = "1.2.840.113549.1.7.4"; + public const string digestedData = "1.2.840.113549.1.7.5"; + public const string encryptedData = "1.2.840.113549.1.7.6"; + // pkcs 9 + public const string contentType = "1.2.840.113549.1.9.3"; + public const string messageDigest = "1.2.840.113549.1.9.4"; + public const string signingTime = "1.2.840.113549.1.9.5"; + public const string countersignature = "1.2.840.113549.1.9.6"; + + public Oid () + { + } + } + + private PKCS7 () + { + } + + static public ASN1 Attribute (string oid, ASN1 value) + { + ASN1 attr = new ASN1 (0x30); + attr.Add (ASN1Convert.FromOid (oid)); + ASN1 aset = attr.Add (new ASN1 (0x31)); + aset.Add (value); + return attr; + } + + static public ASN1 AlgorithmIdentifier (string oid) + { + ASN1 ai = new ASN1 (0x30); + ai.Add (ASN1Convert.FromOid (oid)); + ai.Add (new ASN1 (0x05)); // NULL + return ai; + } + + static public ASN1 AlgorithmIdentifier (string oid, ASN1 parameters) + { + ASN1 ai = new ASN1 (0x30); + ai.Add (ASN1Convert.FromOid (oid)); + ai.Add (parameters); + return ai; + } + + /* + * IssuerAndSerialNumber ::= SEQUENCE { + * issuer Name, + * serialNumber CertificateSerialNumber + * } + */ + static public ASN1 IssuerAndSerialNumber (X509Certificate x509) + { + ASN1 issuer = null; + ASN1 serial = null; + ASN1 cert = new ASN1 (x509.RawData); + int tbs = 0; + bool flag = false; + while (tbs < cert[0].Count) { + ASN1 e = cert[0][tbs++]; + if (e.Tag == 0x02) + serial = e; + else if (e.Tag == 0x30) { + if (flag) { + issuer = e; + break; + } + flag = true; + } + } + ASN1 iasn = new ASN1 (0x30); + iasn.Add (issuer); + iasn.Add (serial); + return iasn; + } + + /* + * ContentInfo ::= SEQUENCE { + * contentType ContentType, + * content [0] EXPLICIT ANY DEFINED BY contentType OPTIONAL + * } + * ContentType ::= OBJECT IDENTIFIER + */ + public class ContentInfo { + + private string contentType; + private ASN1 content; + + public ContentInfo () + { + content = new ASN1 (0xA0); + } + + public ContentInfo (string oid) : this () + { + contentType = oid; + } + + public ContentInfo (byte[] data) + : this (new ASN1 (data)) {} + + public ContentInfo (ASN1 asn1) + { + // SEQUENCE with 1 or 2 elements + if ((asn1.Tag != 0x30) || ((asn1.Count < 1) && (asn1.Count > 2))) + throw new ArgumentException ("Invalid ASN1"); + if (asn1[0].Tag != 0x06) + throw new ArgumentException ("Invalid contentType"); + contentType = ASN1Convert.ToOid (asn1[0]); + if (asn1.Count > 1) { + if (asn1[1].Tag != 0xA0) + throw new ArgumentException ("Invalid content"); + content = asn1[1]; + } + } + + public ASN1 ASN1 { + get { return GetASN1(); } + } + + public ASN1 Content { + get { return content; } + set { content = value; } + } + + public string ContentType { + get { return contentType; } + set { contentType = value; } + } + + internal ASN1 GetASN1 () + { + // ContentInfo ::= SEQUENCE { + ASN1 contentInfo = new ASN1 (0x30); + // contentType ContentType, -> ContentType ::= OBJECT IDENTIFIER + contentInfo.Add (ASN1Convert.FromOid (contentType)); + // content [0] EXPLICIT ANY DEFINED BY contentType OPTIONAL + if ((content != null) && (content.Count > 0)) + contentInfo.Add (content); + return contentInfo; + } + + public byte[] GetBytes () + { + return GetASN1 ().GetBytes (); + } + } + + /* + * EncryptedData ::= SEQUENCE { + * version INTEGER {edVer0(0)} (edVer0), + * encryptedContentInfo EncryptedContentInfo + * } + */ + public class EncryptedData { + private byte _version; + private ContentInfo _content; + private ContentInfo _encryptionAlgorithm; + private byte[] _encrypted; + + public EncryptedData () + { + _version = 0; + } + + public EncryptedData (byte[] data) + : this (new ASN1 (data)) + { + } + + public EncryptedData (ASN1 asn1) : this () + { + if ((asn1.Tag != 0x30) || (asn1.Count < 2)) + throw new ArgumentException ("Invalid EncryptedData"); + + if (asn1 [0].Tag != 0x02) + throw new ArgumentException ("Invalid version"); + _version = asn1 [0].Value [0]; + + ASN1 encryptedContentInfo = asn1 [1]; + if (encryptedContentInfo.Tag != 0x30) + throw new ArgumentException ("missing EncryptedContentInfo"); + + ASN1 contentType = encryptedContentInfo [0]; + if (contentType.Tag != 0x06) + throw new ArgumentException ("missing EncryptedContentInfo.ContentType"); + _content = new ContentInfo (ASN1Convert.ToOid (contentType)); + + ASN1 contentEncryptionAlgorithm = encryptedContentInfo [1]; + if (contentEncryptionAlgorithm.Tag != 0x30) + throw new ArgumentException ("missing EncryptedContentInfo.ContentEncryptionAlgorithmIdentifier"); + _encryptionAlgorithm = new ContentInfo (ASN1Convert.ToOid (contentEncryptionAlgorithm [0])); + _encryptionAlgorithm.Content = contentEncryptionAlgorithm [1]; + + ASN1 encryptedContent = encryptedContentInfo [2]; + if (encryptedContent.Tag != 0x80) + throw new ArgumentException ("missing EncryptedContentInfo.EncryptedContent"); + _encrypted = encryptedContent.Value; + } + + public ASN1 ASN1 { + get { return GetASN1(); } + } + + public ContentInfo ContentInfo { + get { return _content; } + } + + public ContentInfo EncryptionAlgorithm { + get { return _encryptionAlgorithm; } + } + + public byte[] EncryptedContent { + get { + if (_encrypted == null) + return null; + return (byte[]) _encrypted.Clone (); + } + } + + public byte Version { + get { return _version; } + set { _version = value; } + } + + // methods + + internal ASN1 GetASN1 () + { + return null; + } + + public byte[] GetBytes () + { + return GetASN1 ().GetBytes (); + } + } + + /* + * EnvelopedData ::= SEQUENCE { + * version Version, + * recipientInfos RecipientInfos, + * encryptedContentInfo EncryptedContentInfo + * } + * + * RecipientInfos ::= SET OF RecipientInfo + * + * EncryptedContentInfo ::= SEQUENCE { + * contentType ContentType, + * contentEncryptionAlgorithm ContentEncryptionAlgorithmIdentifier, + * encryptedContent [0] IMPLICIT EncryptedContent OPTIONAL + * } + * + * EncryptedContent ::= OCTET STRING + * + */ + public class EnvelopedData { + private byte _version; + private ContentInfo _content; + private ContentInfo _encryptionAlgorithm; + private ArrayList _recipientInfos; + private byte[] _encrypted; + + public EnvelopedData () + { + _version = 0; + _content = new ContentInfo (); + _encryptionAlgorithm = new ContentInfo (); + _recipientInfos = new ArrayList (); + } + + public EnvelopedData (byte[] data) + : this (new ASN1 (data)) + { + } + + public EnvelopedData (ASN1 asn1) : this () + { + if ((asn1[0].Tag != 0x30) || (asn1[0].Count < 3)) + throw new ArgumentException ("Invalid EnvelopedData"); + + if (asn1[0][0].Tag != 0x02) + throw new ArgumentException ("Invalid version"); + _version = asn1[0][0].Value[0]; + + // recipientInfos + + ASN1 recipientInfos = asn1 [0][1]; + if (recipientInfos.Tag != 0x31) + throw new ArgumentException ("missing RecipientInfos"); + for (int i=0; i < recipientInfos.Count; i++) { + ASN1 recipientInfo = recipientInfos [i]; + _recipientInfos.Add (new RecipientInfo (recipientInfo)); + } + + ASN1 encryptedContentInfo = asn1[0][2]; + if (encryptedContentInfo.Tag != 0x30) + throw new ArgumentException ("missing EncryptedContentInfo"); + + ASN1 contentType = encryptedContentInfo [0]; + if (contentType.Tag != 0x06) + throw new ArgumentException ("missing EncryptedContentInfo.ContentType"); + _content = new ContentInfo (ASN1Convert.ToOid (contentType)); + + ASN1 contentEncryptionAlgorithm = encryptedContentInfo [1]; + if (contentEncryptionAlgorithm.Tag != 0x30) + throw new ArgumentException ("missing EncryptedContentInfo.ContentEncryptionAlgorithmIdentifier"); + _encryptionAlgorithm = new ContentInfo (ASN1Convert.ToOid (contentEncryptionAlgorithm [0])); + _encryptionAlgorithm.Content = contentEncryptionAlgorithm [1]; + + ASN1 encryptedContent = encryptedContentInfo [2]; + if (encryptedContent.Tag != 0x80) + throw new ArgumentException ("missing EncryptedContentInfo.EncryptedContent"); + _encrypted = encryptedContent.Value; + } + + public ArrayList RecipientInfos { + get { return _recipientInfos; } + } + + public ASN1 ASN1 { + get { return GetASN1(); } + } + + public ContentInfo ContentInfo { + get { return _content; } + } + + public ContentInfo EncryptionAlgorithm { + get { return _encryptionAlgorithm; } + } + + public byte[] EncryptedContent { + get { + if (_encrypted == null) + return null; + return (byte[]) _encrypted.Clone (); + } + } + + public byte Version { + get { return _version; } + set { _version = value; } + } + + internal ASN1 GetASN1 () + { + // SignedData ::= SEQUENCE { + ASN1 signedData = new ASN1 (0x30); + // version Version -> Version ::= INTEGER +/* byte[] ver = { _version }; + signedData.Add (new ASN1 (0x02, ver)); + // digestAlgorithms DigestAlgorithmIdentifiers -> DigestAlgorithmIdentifiers ::= SET OF DigestAlgorithmIdentifier + ASN1 digestAlgorithms = signedData.Add (new ASN1 (0x31)); + if (hashAlgorithm != null) { + string hashOid = CryptoConfig.MapNameToOid (hashAlgorithm); + digestAlgorithms.Add (AlgorithmIdentifier (hashOid)); + } + + // contentInfo ContentInfo, + ASN1 ci = contentInfo.ASN1; + signedData.Add (ci); + if ((mda == null) && (hashAlgorithm != null)) { + // automatically add the messageDigest authenticated attribute + HashAlgorithm ha = HashAlgorithm.Create (hashAlgorithm); + byte[] idcHash = ha.ComputeHash (ci[1][0].Value); + ASN1 md = new ASN1 (0x30); + mda = Attribute (messageDigest, md.Add (new ASN1 (0x04, idcHash))); + signerInfo.AuthenticatedAttributes.Add (mda); + } + + // certificates [0] IMPLICIT ExtendedCertificatesAndCertificates OPTIONAL, + if (certs.Count > 0) { + ASN1 a0 = signedData.Add (new ASN1 (0xA0)); + foreach (X509Certificate x in certs) + a0.Add (new ASN1 (x.RawData)); + } + // crls [1] IMPLICIT CertificateRevocationLists OPTIONAL, + if (crls.Count > 0) { + ASN1 a1 = signedData.Add (new ASN1 (0xA1)); + foreach (byte[] crl in crls) + a1.Add (new ASN1 (crl)); + } + // signerInfos SignerInfos -> SignerInfos ::= SET OF SignerInfo + ASN1 signerInfos = signedData.Add (new ASN1 (0x31)); + if (signerInfo.Key != null) + signerInfos.Add (signerInfo.ASN1);*/ + return signedData; + } + + public byte[] GetBytes () { + return GetASN1 ().GetBytes (); + } + } + + /* RecipientInfo ::= SEQUENCE { + * version Version, + * issuerAndSerialNumber IssuerAndSerialNumber, + * keyEncryptionAlgorithm KeyEncryptionAlgorithmIdentifier, + * encryptedKey EncryptedKey + * } + * + * KeyEncryptionAlgorithmIdentifier ::= AlgorithmIdentifier + * + * EncryptedKey ::= OCTET STRING + */ + public class RecipientInfo { + + private int _version; + private string _oid; + private byte[] _key; + private byte[] _ski; + private string _issuer; + private byte[] _serial; + + public RecipientInfo () {} + + public RecipientInfo (ASN1 data) + { + if (data.Tag != 0x30) + throw new ArgumentException ("Invalid RecipientInfo"); + + ASN1 version = data [0]; + if (version.Tag != 0x02) + throw new ArgumentException ("missing Version"); + _version = version.Value [0]; + + // issuerAndSerialNumber IssuerAndSerialNumber + ASN1 subjectIdentifierType = data [1]; + if ((subjectIdentifierType.Tag == 0x80) && (_version == 3)) { + _ski = subjectIdentifierType.Value; + } + else { + _issuer = X501.ToString (subjectIdentifierType [0]); + _serial = subjectIdentifierType [1].Value; + } + + ASN1 keyEncryptionAlgorithm = data [2]; + _oid = ASN1Convert.ToOid (keyEncryptionAlgorithm [0]); + + ASN1 encryptedKey = data [3]; + _key = encryptedKey.Value; + } + + public string Oid { + get { return _oid; } + } + + public byte[] Key { + get { + if (_key == null) + return null; + return (byte[]) _key.Clone (); + } + } + + public byte[] SubjectKeyIdentifier { + get { + if (_ski == null) + return null; + return (byte[]) _ski.Clone (); + } + } + + public string Issuer { + get { return _issuer; } + } + + public byte[] Serial { + get { + if (_serial == null) + return null; + return (byte[]) _serial.Clone (); + } + } + + public int Version { + get { return _version; } + } + } + + /* + * SignedData ::= SEQUENCE { + * version Version, + * digestAlgorithms DigestAlgorithmIdentifiers, + * contentInfo ContentInfo, + * certificates [0] IMPLICIT ExtendedCertificatesAndCertificates OPTIONAL, + * crls [1] IMPLICIT CertificateRevocationLists OPTIONAL, + * signerInfos SignerInfos + * } + */ + public class SignedData { + private byte version; + private string hashAlgorithm; + private ContentInfo contentInfo; + private X509CertificateCollection certs; + private ArrayList crls; + private SignerInfo signerInfo; + private bool mda; + private bool signed; + + public SignedData () + { + version = 1; + contentInfo = new ContentInfo (); + certs = new X509CertificateCollection (); + crls = new ArrayList (); + signerInfo = new SignerInfo (); + mda = true; + signed = false; + } + + public SignedData (byte[] data) + : this (new ASN1 (data)) + { + } + + public SignedData (ASN1 asn1) + { + if ((asn1[0].Tag != 0x30) || (asn1[0].Count < 4)) + throw new ArgumentException ("Invalid SignedData"); + + if (asn1[0][0].Tag != 0x02) + throw new ArgumentException ("Invalid version"); + version = asn1[0][0].Value[0]; + + contentInfo = new ContentInfo (asn1[0][2]); + + int n = 3; + certs = new X509CertificateCollection (); + if (asn1[0][n].Tag == 0xA0) { + for (int i=0; i < asn1[0][n].Count; i++) + certs.Add (new X509Certificate (asn1[0][n][i].GetBytes ())); + n++; + } + + crls = new ArrayList (); + if (asn1[0][n].Tag == 0xA1) { + for (int i=0; i < asn1[0][n].Count; i++) + crls.Add (asn1[0][n][i].GetBytes ()); + n++; + } + + if (asn1[0][n].Count > 0) + signerInfo = new SignerInfo (asn1[0][n]); + else + signerInfo = new SignerInfo (); + + // Exchange hash algorithm Oid from SignerInfo + if (signerInfo.HashName != null) { + HashName = OidToName(signerInfo.HashName); + } + + // Check if SignerInfo has authenticated attributes + mda = (signerInfo.AuthenticatedAttributes.Count > 0); + } + + public ASN1 ASN1 { + get { return GetASN1(); } + } + + public X509CertificateCollection Certificates { + get { return certs; } + } + + public ContentInfo ContentInfo { + get { return contentInfo; } + } + + public ArrayList Crls { + get { return crls; } + } + + public string HashName { + get { return hashAlgorithm; } + // todo add validation + set { + hashAlgorithm = value; + signerInfo.HashName = value; + } + } + + public SignerInfo SignerInfo { + get { return signerInfo; } + } + + public byte Version { + get { return version; } + set { version = value; } + } + + public bool UseAuthenticatedAttributes { + get { return mda; } + set { mda = value; } + } + + public bool VerifySignature (AsymmetricAlgorithm aa) + { + if (aa == null) { + return false; + } + + RSAPKCS1SignatureDeformatter r = new RSAPKCS1SignatureDeformatter (aa); + r.SetHashAlgorithm (hashAlgorithm); + HashAlgorithm ha = HashAlgorithm.Create (hashAlgorithm); + + byte[] signature = signerInfo.Signature; + byte[] hash = null; + + if (mda) { + ASN1 asn = new ASN1 (0x31); + foreach (ASN1 attr in signerInfo.AuthenticatedAttributes) + asn.Add (attr); + + hash = ha.ComputeHash (asn.GetBytes ()); + } else { + hash = ha.ComputeHash (contentInfo.Content[0].Value); + } + + if (hash != null && signature != null) { + return r.VerifySignature (hash, signature); + } + return false; + } + + internal string OidToName (string oid) + { + switch (oid) { + case "1.3.14.3.2.26" : + return "SHA1"; + case "1.2.840.113549.2.2" : + return "MD2"; + case "1.2.840.113549.2.5" : + return "MD5"; + case "2.16.840.1.101.3.4.1" : + return "SHA256"; + case "2.16.840.1.101.3.4.2" : + return "SHA384"; + case "2.16.840.1.101.3.4.3" : + return "SHA512"; + default : + break; + } + // Unknown Oid + return oid; + } + + internal ASN1 GetASN1 () + { + // SignedData ::= SEQUENCE { + ASN1 signedData = new ASN1 (0x30); + // version Version -> Version ::= INTEGER + byte[] ver = { version }; + signedData.Add (new ASN1 (0x02, ver)); + // digestAlgorithms DigestAlgorithmIdentifiers -> DigestAlgorithmIdentifiers ::= SET OF DigestAlgorithmIdentifier + ASN1 digestAlgorithms = signedData.Add (new ASN1 (0x31)); + if (hashAlgorithm != null) { + string hashOid = CryptoConfig.MapNameToOID (hashAlgorithm); + digestAlgorithms.Add (AlgorithmIdentifier (hashOid)); + } + + // contentInfo ContentInfo, + ASN1 ci = contentInfo.ASN1; + signedData.Add (ci); + if (!signed && (hashAlgorithm != null)) { + if (mda) { + // Use authenticated attributes for signature + + // Automatically add the contentType authenticated attribute + ASN1 ctattr = Attribute (Oid.contentType, ci[0]); + signerInfo.AuthenticatedAttributes.Add (ctattr); + + // Automatically add the messageDigest authenticated attribute + HashAlgorithm ha = HashAlgorithm.Create (hashAlgorithm); + byte[] idcHash = ha.ComputeHash (ci[1][0].Value); + ASN1 md = new ASN1 (0x30); + ASN1 mdattr = Attribute (Oid.messageDigest, md.Add (new ASN1 (0x04, idcHash))); + signerInfo.AuthenticatedAttributes.Add (mdattr); + } else { + // Don't use authenticated attributes for signature -- signature is content + RSAPKCS1SignatureFormatter r = new RSAPKCS1SignatureFormatter (signerInfo.Key); + r.SetHashAlgorithm (hashAlgorithm); + HashAlgorithm ha = HashAlgorithm.Create (hashAlgorithm); + byte[] sig = ha.ComputeHash (ci[1][0].Value); + signerInfo.Signature = r.CreateSignature (sig); + } + signed = true; + } + + // certificates [0] IMPLICIT ExtendedCertificatesAndCertificates OPTIONAL, + if (certs.Count > 0) { + ASN1 a0 = signedData.Add (new ASN1 (0xA0)); + foreach (X509Certificate x in certs) + a0.Add (new ASN1 (x.RawData)); + } + // crls [1] IMPLICIT CertificateRevocationLists OPTIONAL, + if (crls.Count > 0) { + ASN1 a1 = signedData.Add (new ASN1 (0xA1)); + foreach (byte[] crl in crls) + a1.Add (new ASN1 (crl)); + } + // signerInfos SignerInfos -> SignerInfos ::= SET OF SignerInfo + ASN1 signerInfos = signedData.Add (new ASN1 (0x31)); + if (signerInfo.Key != null) + signerInfos.Add (signerInfo.ASN1); + return signedData; + } + + public byte[] GetBytes () + { + return GetASN1 ().GetBytes (); + } + } + + /* + * SignerInfo ::= SEQUENCE { + * version Version, + * issuerAndSerialNumber IssuerAndSerialNumber, + * digestAlgorithm DigestAlgorithmIdentifier, + * authenticatedAttributes [0] IMPLICIT Attributes OPTIONAL, + * digestEncryptionAlgorithm DigestEncryptionAlgorithmIdentifier, + * encryptedDigest EncryptedDigest, + * unauthenticatedAttributes [1] IMPLICIT Attributes OPTIONAL + * } + * + * For version == 3 issuerAndSerialNumber may be replaced by ... + */ + public class SignerInfo { + + private byte version; + private X509Certificate x509; + private string hashAlgorithm; + private AsymmetricAlgorithm key; + private ArrayList authenticatedAttributes; + private ArrayList unauthenticatedAttributes; + private byte[] signature; + private string issuer; + private byte[] serial; + private byte[] ski; + + public SignerInfo () + { + version = 1; + authenticatedAttributes = new ArrayList (); + unauthenticatedAttributes = new ArrayList (); + } + + public SignerInfo (byte[] data) + : this (new ASN1 (data)) {} + + // TODO: INCOMPLETE + public SignerInfo (ASN1 asn1) : this () + { + if ((asn1[0].Tag != 0x30) || (asn1[0].Count < 5)) + throw new ArgumentException ("Invalid SignedData"); + + // version Version + if (asn1[0][0].Tag != 0x02) + throw new ArgumentException ("Invalid version"); + version = asn1[0][0].Value[0]; + + // issuerAndSerialNumber IssuerAndSerialNumber + ASN1 subjectIdentifierType = asn1 [0][1]; + if ((subjectIdentifierType.Tag == 0x80) && (version == 3)) { + ski = subjectIdentifierType.Value; + } + else { + issuer = X501.ToString (subjectIdentifierType [0]); + serial = subjectIdentifierType [1].Value; + } + + // digestAlgorithm DigestAlgorithmIdentifier + ASN1 digestAlgorithm = asn1 [0][2]; + hashAlgorithm = ASN1Convert.ToOid (digestAlgorithm [0]); + + // authenticatedAttributes [0] IMPLICIT Attributes OPTIONAL + int n = 3; + ASN1 authAttributes = asn1 [0][n]; + if (authAttributes.Tag == 0xA0) { + n++; + for (int i=0; i < authAttributes.Count; i++) + authenticatedAttributes.Add (authAttributes [i]); + } + + // digestEncryptionAlgorithm DigestEncryptionAlgorithmIdentifier + n++; + // ASN1 digestEncryptionAlgorithm = asn1 [0][n++]; + // string digestEncryptionAlgorithmOid = ASN1Convert.ToOid (digestEncryptionAlgorithm [0]); + + // encryptedDigest EncryptedDigest + ASN1 encryptedDigest = asn1 [0][n++]; + if (encryptedDigest.Tag == 0x04) + signature = encryptedDigest.Value; + + // unauthenticatedAttributes [1] IMPLICIT Attributes OPTIONAL + ASN1 unauthAttributes = asn1 [0][n]; + if ((unauthAttributes != null) && (unauthAttributes.Tag == 0xA1)) { + for (int i=0; i < unauthAttributes.Count; i++) + unauthenticatedAttributes.Add (unauthAttributes [i]); + } + } + + public string IssuerName { + get { return issuer; } + } + + public byte[] SerialNumber { + get { + if (serial == null) + return null; + return (byte[]) serial.Clone (); + } + } + + public byte[] SubjectKeyIdentifier { + get { + if (ski == null) + return null; + return (byte[]) ski.Clone (); + } + } + + public ASN1 ASN1 { + get { return GetASN1(); } + } + + public ArrayList AuthenticatedAttributes { + get { return authenticatedAttributes; } + } + + public X509Certificate Certificate { + get { return x509; } + set { x509 = value; } + } + + public string HashName { + get { return hashAlgorithm; } + set { hashAlgorithm = value; } + } + + public AsymmetricAlgorithm Key { + get { return key; } + set { key = value; } + } + + public byte[] Signature { + get { + if (signature == null) + return null; + return (byte[]) signature.Clone (); + } + + set { + if (value != null) { + signature = (byte[]) value.Clone (); + } + } + } + + public ArrayList UnauthenticatedAttributes { + get { return unauthenticatedAttributes; } + } + + public byte Version { + get { return version; } + set { version = value; } + } + + internal ASN1 GetASN1 () + { + if ((key == null) || (hashAlgorithm == null)) + return null; + byte[] ver = { version }; + ASN1 signerInfo = new ASN1 (0x30); + // version Version -> Version ::= INTEGER + signerInfo.Add (new ASN1 (0x02, ver)); + // issuerAndSerialNumber IssuerAndSerialNumber, + signerInfo.Add (PKCS7.IssuerAndSerialNumber (x509)); + // digestAlgorithm DigestAlgorithmIdentifier, + string hashOid = CryptoConfig.MapNameToOID (hashAlgorithm); + signerInfo.Add (AlgorithmIdentifier (hashOid)); + // authenticatedAttributes [0] IMPLICIT Attributes OPTIONAL, + ASN1 aa = null; + if (authenticatedAttributes.Count > 0) { + aa = signerInfo.Add (new ASN1 (0xA0)); + authenticatedAttributes.Sort(new SortedSet ()); + foreach (ASN1 attr in authenticatedAttributes) + aa.Add (attr); + } + // digestEncryptionAlgorithm DigestEncryptionAlgorithmIdentifier, + if (key is RSA) { + signerInfo.Add (AlgorithmIdentifier (PKCS7.Oid.rsaEncryption)); + + if (aa != null) { + // Calculate the signature here; otherwise it must be set from SignedData + RSAPKCS1SignatureFormatter r = new RSAPKCS1SignatureFormatter (key); + r.SetHashAlgorithm (hashAlgorithm); + byte[] tbs = aa.GetBytes (); + tbs [0] = 0x31; // not 0xA0 for signature + HashAlgorithm ha = HashAlgorithm.Create (hashAlgorithm); + byte[] tbsHash = ha.ComputeHash (tbs); + signature = r.CreateSignature (tbsHash); + } + } + else if (key is DSA) { + throw new NotImplementedException ("not yet"); + } + else + throw new CryptographicException ("Unknown assymetric algorithm"); + // encryptedDigest EncryptedDigest, + signerInfo.Add (new ASN1 (0x04, signature)); + // unauthenticatedAttributes [1] IMPLICIT Attributes OPTIONAL + if (unauthenticatedAttributes.Count > 0) { + ASN1 ua = signerInfo.Add (new ASN1 (0xA1)); + unauthenticatedAttributes.Sort(new SortedSet ()); + foreach (ASN1 attr in unauthenticatedAttributes) + ua.Add (attr); + } + return signerInfo; + } + + public byte[] GetBytes () + { + return GetASN1 ().GetBytes (); + } + } + + internal class SortedSet : IComparer { + + public int Compare (object x, object y) + { + if (x == null) + return (y == null) ? 0 : -1; + else if (y == null) + return 1; + + ASN1 xx = x as ASN1; + ASN1 yy = y as ASN1; + + if ((xx == null) || (yy == null)) { + throw new ArgumentException (("Invalid objects.")); + } + + byte[] xb = xx.GetBytes (); + byte[] yb = yy.GetBytes (); + + for (int i = 0; i < xb.Length; i++) { + if (i == yb.Length) + break; + + if (xb[i] == yb[i]) + continue; + + return (xb[i] < yb[i]) ? -1 : 1; + } + + // The arrays are equal up to the shortest of them. + if (xb.Length > yb.Length) + return 1; + else if (xb.Length < yb.Length) + return -1; + + return 0; + } + } + } +} diff --git a/Emby.Server.Implementations/Cryptography/PKCS8.cs b/Emby.Server.Implementations/Cryptography/PKCS8.cs new file mode 100644 index 000000000..7e9a27298 --- /dev/null +++ b/Emby.Server.Implementations/Cryptography/PKCS8.cs @@ -0,0 +1,495 @@ +// +// PKCS8.cs: PKCS #8 - Private-Key Information Syntax Standard +// ftp://ftp.rsasecurity.com/pub/pkcs/doc/pkcs-8.doc +// +// Author: +// Sebastien Pouliot <sebastien@xamarin.com> +// +// (C) 2003 Motus Technologies Inc. (http://www.motus.com) +// Copyright (C) 2004-2006 Novell Inc. (http://www.novell.com) +// Copyright 2013 Xamarin Inc. (http://www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections; +using System.Security.Cryptography; + +namespace Emby.Server.Core.Cryptography +{ + + public sealed class PKCS8 { + + public enum KeyInfo { + PrivateKey, + EncryptedPrivateKey, + Unknown + } + + private PKCS8 () + { + } + + static public KeyInfo GetType (byte[] data) + { + if (data == null) + throw new ArgumentNullException ("data"); + + KeyInfo ki = KeyInfo.Unknown; + try { + ASN1 top = new ASN1 (data); + if ((top.Tag == 0x30) && (top.Count > 0)) { + ASN1 firstLevel = top [0]; + switch (firstLevel.Tag) { + case 0x02: + ki = KeyInfo.PrivateKey; + break; + case 0x30: + ki = KeyInfo.EncryptedPrivateKey; + break; + } + } + } + catch { + throw new CryptographicException ("invalid ASN.1 data"); + } + return ki; + } + + /* + * PrivateKeyInfo ::= SEQUENCE { + * version Version, + * privateKeyAlgorithm PrivateKeyAlgorithmIdentifier, + * privateKey PrivateKey, + * attributes [0] IMPLICIT Attributes OPTIONAL + * } + * + * Version ::= INTEGER + * + * PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier + * + * PrivateKey ::= OCTET STRING + * + * Attributes ::= SET OF Attribute + */ + public class PrivateKeyInfo { + + private int _version; + private string _algorithm; + private byte[] _key; + private ArrayList _list; + + public PrivateKeyInfo () + { + _version = 0; + _list = new ArrayList (); + } + + public PrivateKeyInfo (byte[] data) : this () + { + Decode (data); + } + + // properties + + public string Algorithm { + get { return _algorithm; } + set { _algorithm = value; } + } + + public ArrayList Attributes { + get { return _list; } + } + + public byte[] PrivateKey { + get { + if (_key == null) + return null; + return (byte[]) _key.Clone (); + } + set { + if (value == null) + throw new ArgumentNullException ("PrivateKey"); + _key = (byte[]) value.Clone (); + } + } + + public int Version { + get { return _version; } + set { + if (value < 0) + throw new ArgumentOutOfRangeException ("negative version"); + _version = value; + } + } + + // methods + + private void Decode (byte[] data) + { + ASN1 privateKeyInfo = new ASN1 (data); + if (privateKeyInfo.Tag != 0x30) + throw new CryptographicException ("invalid PrivateKeyInfo"); + + ASN1 version = privateKeyInfo [0]; + if (version.Tag != 0x02) + throw new CryptographicException ("invalid version"); + _version = version.Value [0]; + + ASN1 privateKeyAlgorithm = privateKeyInfo [1]; + if (privateKeyAlgorithm.Tag != 0x30) + throw new CryptographicException ("invalid algorithm"); + + ASN1 algorithm = privateKeyAlgorithm [0]; + if (algorithm.Tag != 0x06) + throw new CryptographicException ("missing algorithm OID"); + _algorithm = ASN1Convert.ToOid (algorithm); + + ASN1 privateKey = privateKeyInfo [2]; + _key = privateKey.Value; + + // attributes [0] IMPLICIT Attributes OPTIONAL + if (privateKeyInfo.Count > 3) { + ASN1 attributes = privateKeyInfo [3]; + for (int i=0; i < attributes.Count; i++) { + _list.Add (attributes [i]); + } + } + } + + public byte[] GetBytes () + { + ASN1 privateKeyAlgorithm = new ASN1 (0x30); + privateKeyAlgorithm.Add (ASN1Convert.FromOid (_algorithm)); + privateKeyAlgorithm.Add (new ASN1 (0x05)); // ASN.1 NULL + + ASN1 pki = new ASN1 (0x30); + pki.Add (new ASN1 (0x02, new byte [1] { (byte) _version })); + pki.Add (privateKeyAlgorithm); + pki.Add (new ASN1 (0x04, _key)); + + if (_list.Count > 0) { + ASN1 attributes = new ASN1 (0xA0); + foreach (ASN1 attribute in _list) { + attributes.Add (attribute); + } + pki.Add (attributes); + } + + return pki.GetBytes (); + } + + // static methods + + static private byte[] RemoveLeadingZero (byte[] bigInt) + { + int start = 0; + int length = bigInt.Length; + if (bigInt [0] == 0x00) { + start = 1; + length--; + } + byte[] bi = new byte [length]; + Buffer.BlockCopy (bigInt, start, bi, 0, length); + return bi; + } + + static private byte[] Normalize (byte[] bigInt, int length) + { + if (bigInt.Length == length) + return bigInt; + else if (bigInt.Length > length) + return RemoveLeadingZero (bigInt); + else { + // pad with 0 + byte[] bi = new byte [length]; + Buffer.BlockCopy (bigInt, 0, bi, (length - bigInt.Length), bigInt.Length); + return bi; + } + } + + /* + * RSAPrivateKey ::= SEQUENCE { + * version Version, + * modulus INTEGER, -- n + * publicExponent INTEGER, -- e + * privateExponent INTEGER, -- d + * prime1 INTEGER, -- p + * prime2 INTEGER, -- q + * exponent1 INTEGER, -- d mod (p-1) + * exponent2 INTEGER, -- d mod (q-1) + * coefficient INTEGER, -- (inverse of q) mod p + * otherPrimeInfos OtherPrimeInfos OPTIONAL + * } + */ + static public RSA DecodeRSA (byte[] keypair) + { + ASN1 privateKey = new ASN1 (keypair); + if (privateKey.Tag != 0x30) + throw new CryptographicException ("invalid private key format"); + + ASN1 version = privateKey [0]; + if (version.Tag != 0x02) + throw new CryptographicException ("missing version"); + + if (privateKey.Count < 9) + throw new CryptographicException ("not enough key parameters"); + + RSAParameters param = new RSAParameters (); + // note: MUST remove leading 0 - else MS wont import the key + param.Modulus = RemoveLeadingZero (privateKey [1].Value); + int keysize = param.Modulus.Length; + int keysize2 = (keysize >> 1); // half-size + // size must be normalized - else MS wont import the key + param.D = Normalize (privateKey [3].Value, keysize); + param.DP = Normalize (privateKey [6].Value, keysize2); + param.DQ = Normalize (privateKey [7].Value, keysize2); + param.Exponent = RemoveLeadingZero (privateKey [2].Value); + param.InverseQ = Normalize (privateKey [8].Value, keysize2); + param.P = Normalize (privateKey [4].Value, keysize2); + param.Q = Normalize (privateKey [5].Value, keysize2); + + RSA rsa = null; + try { + rsa = RSA.Create (); + rsa.ImportParameters (param); + } + catch (CryptographicException) { + // this may cause problem when this code is run under + // the SYSTEM identity on Windows (e.g. ASP.NET). See + // http://bugzilla.ximian.com/show_bug.cgi?id=77559 + CspParameters csp = new CspParameters(); + csp.Flags = CspProviderFlags.UseMachineKeyStore; + rsa = new RSACryptoServiceProvider(csp); + rsa.ImportParameters(param); + } + return rsa; + } + + /* + * RSAPrivateKey ::= SEQUENCE { + * version Version, + * modulus INTEGER, -- n + * publicExponent INTEGER, -- e + * privateExponent INTEGER, -- d + * prime1 INTEGER, -- p + * prime2 INTEGER, -- q + * exponent1 INTEGER, -- d mod (p-1) + * exponent2 INTEGER, -- d mod (q-1) + * coefficient INTEGER, -- (inverse of q) mod p + * otherPrimeInfos OtherPrimeInfos OPTIONAL + * } + */ + static public byte[] Encode (RSA rsa) + { + RSAParameters param = rsa.ExportParameters (true); + + ASN1 rsaPrivateKey = new ASN1 (0x30); + rsaPrivateKey.Add (new ASN1 (0x02, new byte [1] { 0x00 })); + rsaPrivateKey.Add (ASN1Convert.FromUnsignedBigInteger (param.Modulus)); + rsaPrivateKey.Add (ASN1Convert.FromUnsignedBigInteger (param.Exponent)); + rsaPrivateKey.Add (ASN1Convert.FromUnsignedBigInteger (param.D)); + rsaPrivateKey.Add (ASN1Convert.FromUnsignedBigInteger (param.P)); + rsaPrivateKey.Add (ASN1Convert.FromUnsignedBigInteger (param.Q)); + rsaPrivateKey.Add (ASN1Convert.FromUnsignedBigInteger (param.DP)); + rsaPrivateKey.Add (ASN1Convert.FromUnsignedBigInteger (param.DQ)); + rsaPrivateKey.Add (ASN1Convert.FromUnsignedBigInteger (param.InverseQ)); + + return rsaPrivateKey.GetBytes (); + } + + // DSA only encode it's X private key inside an ASN.1 INTEGER (Hint: Tag == 0x02) + // which isn't enough for rebuilding the keypair. The other parameters + // can be found (98% of the time) in the X.509 certificate associated + // with the private key or (2% of the time) the parameters are in it's + // issuer X.509 certificate (not supported in the .NET framework). + static public DSA DecodeDSA (byte[] privateKey, DSAParameters dsaParameters) + { + ASN1 pvk = new ASN1 (privateKey); + if (pvk.Tag != 0x02) + throw new CryptographicException ("invalid private key format"); + + // X is ALWAYS 20 bytes (no matter if the key length is 512 or 1024 bits) + dsaParameters.X = Normalize (pvk.Value, 20); + DSA dsa = DSA.Create (); + dsa.ImportParameters (dsaParameters); + return dsa; + } + + static public byte[] Encode (DSA dsa) + { + DSAParameters param = dsa.ExportParameters (true); + return ASN1Convert.FromUnsignedBigInteger (param.X).GetBytes (); + } + + static public byte[] Encode (AsymmetricAlgorithm aa) + { + if (aa is RSA) + return Encode ((RSA)aa); + else if (aa is DSA) + return Encode ((DSA)aa); + else + throw new CryptographicException ("Unknown asymmetric algorithm {0}", aa.ToString ()); + } + } + + /* + * EncryptedPrivateKeyInfo ::= SEQUENCE { + * encryptionAlgorithm EncryptionAlgorithmIdentifier, + * encryptedData EncryptedData + * } + * + * EncryptionAlgorithmIdentifier ::= AlgorithmIdentifier + * + * EncryptedData ::= OCTET STRING + * + * -- + * AlgorithmIdentifier ::= SEQUENCE { + * algorithm OBJECT IDENTIFIER, + * parameters ANY DEFINED BY algorithm OPTIONAL + * } + * + * -- from PKCS#5 + * PBEParameter ::= SEQUENCE { + * salt OCTET STRING SIZE(8), + * iterationCount INTEGER + * } + */ + public class EncryptedPrivateKeyInfo { + + private string _algorithm; + private byte[] _salt; + private int _iterations; + private byte[] _data; + + public EncryptedPrivateKeyInfo () {} + + public EncryptedPrivateKeyInfo (byte[] data) : this () + { + Decode (data); + } + + // properties + + public string Algorithm { + get { return _algorithm; } + set { _algorithm = value; } + } + + public byte[] EncryptedData { + get { return (_data == null) ? null : (byte[]) _data.Clone (); } + set { _data = (value == null) ? null : (byte[]) value.Clone (); } + } + + public byte[] Salt { + get { + if (_salt == null) { + RandomNumberGenerator rng = RandomNumberGenerator.Create (); + _salt = new byte [8]; + rng.GetBytes (_salt); + } + return (byte[]) _salt.Clone (); + } + set { _salt = (byte[]) value.Clone (); } + } + + public int IterationCount { + get { return _iterations; } + set { + if (value < 0) + throw new ArgumentOutOfRangeException ("IterationCount", "Negative"); + _iterations = value; + } + } + + // methods + + private void Decode (byte[] data) + { + ASN1 encryptedPrivateKeyInfo = new ASN1 (data); + if (encryptedPrivateKeyInfo.Tag != 0x30) + throw new CryptographicException ("invalid EncryptedPrivateKeyInfo"); + + ASN1 encryptionAlgorithm = encryptedPrivateKeyInfo [0]; + if (encryptionAlgorithm.Tag != 0x30) + throw new CryptographicException ("invalid encryptionAlgorithm"); + ASN1 algorithm = encryptionAlgorithm [0]; + if (algorithm.Tag != 0x06) + throw new CryptographicException ("invalid algorithm"); + _algorithm = ASN1Convert.ToOid (algorithm); + // parameters ANY DEFINED BY algorithm OPTIONAL + if (encryptionAlgorithm.Count > 1) { + ASN1 parameters = encryptionAlgorithm [1]; + if (parameters.Tag != 0x30) + throw new CryptographicException ("invalid parameters"); + + ASN1 salt = parameters [0]; + if (salt.Tag != 0x04) + throw new CryptographicException ("invalid salt"); + _salt = salt.Value; + + ASN1 iterationCount = parameters [1]; + if (iterationCount.Tag != 0x02) + throw new CryptographicException ("invalid iterationCount"); + _iterations = ASN1Convert.ToInt32 (iterationCount); + } + + ASN1 encryptedData = encryptedPrivateKeyInfo [1]; + if (encryptedData.Tag != 0x04) + throw new CryptographicException ("invalid EncryptedData"); + _data = encryptedData.Value; + } + + // Note: PKCS#8 doesn't define how to generate the key required for encryption + // so you're on your own. Just don't try to copy the big guys too much ;) + // Netscape: http://www.cs.auckland.ac.nz/~pgut001/pubs/netscape.txt + // Microsoft: http://www.cs.auckland.ac.nz/~pgut001/pubs/breakms.txt + public byte[] GetBytes () + { + if (_algorithm == null) + throw new CryptographicException ("No algorithm OID specified"); + + ASN1 encryptionAlgorithm = new ASN1 (0x30); + encryptionAlgorithm.Add (ASN1Convert.FromOid (_algorithm)); + + // parameters ANY DEFINED BY algorithm OPTIONAL + if ((_iterations > 0) || (_salt != null)) { + ASN1 salt = new ASN1 (0x04, _salt); + ASN1 iterations = ASN1Convert.FromInt32 (_iterations); + + ASN1 parameters = new ASN1 (0x30); + parameters.Add (salt); + parameters.Add (iterations); + encryptionAlgorithm.Add (parameters); + } + + // encapsulates EncryptedData into an OCTET STRING + ASN1 encryptedData = new ASN1 (0x04, _data); + + ASN1 encryptedPrivateKeyInfo = new ASN1 (0x30); + encryptedPrivateKeyInfo.Add (encryptionAlgorithm); + encryptedPrivateKeyInfo.Add (encryptedData); + + return encryptedPrivateKeyInfo.GetBytes (); + } + } + } +} diff --git a/Emby.Server.Implementations/Cryptography/PfxGenerator.cs b/Emby.Server.Implementations/Cryptography/PfxGenerator.cs new file mode 100644 index 000000000..2d1dd649e --- /dev/null +++ b/Emby.Server.Implementations/Cryptography/PfxGenerator.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections; +using System.Security.Cryptography; + +namespace Emby.Server.Core.Cryptography +{ + public class PFXGenerator + { + // http://www.freekpaans.nl/2015/04/creating-self-signed-x-509-certificates-using-mono-security/ + public static byte[] GeneratePfx(string certificateName, string password) + { + byte[] sn = GenerateSerialNumber(); + string subject = string.Format("CN={0}", certificateName); + + DateTime notBefore = DateTime.Now; + DateTime notAfter = DateTime.Now.AddYears(20); + + RSA subjectKey = new RSACryptoServiceProvider(2048); + + + string hashName = "SHA256"; + + X509CertificateBuilder cb = new X509CertificateBuilder(3); + cb.SerialNumber = sn; + cb.IssuerName = subject; + cb.NotBefore = notBefore; + cb.NotAfter = notAfter; + cb.SubjectName = subject; + cb.SubjectPublicKey = subjectKey; + cb.Hash = hashName; + + byte[] rawcert = cb.Sign(subjectKey); + + + PKCS12 p12 = new PKCS12(); + p12.Password = password; + + Hashtable attributes = GetAttributes(); + + p12.AddCertificate(new X509Certificate(rawcert), attributes); + p12.AddPkcs8ShroudedKeyBag(subjectKey, attributes); + + return p12.GetBytes(); + } + + private static Hashtable GetAttributes() + { + ArrayList list = new ArrayList(); + // we use a fixed array to avoid endianess issues + // (in case some tools requires the ID to be 1). + list.Add(new byte[4] { 1, 0, 0, 0 }); + Hashtable attributes = new Hashtable(1); + attributes.Add(PKCS9.localKeyId, list); + return attributes; + } + + private static byte[] GenerateSerialNumber() + { + byte[] sn = Guid.NewGuid().ToByteArray(); + + //must be positive + if ((sn[0] & 0x80) == 0x80) + sn[0] -= 0x80; + return sn; + } + + public static byte[] GetCertificateForBytes(byte[] pfx, string password) + { + var pkcs = new PKCS12(pfx, password); + var cert = pkcs.GetCertificate(GetAttributes()); + + return cert.RawData; + } + } +} diff --git a/Emby.Server.Implementations/Cryptography/X501Name.cs b/Emby.Server.Implementations/Cryptography/X501Name.cs new file mode 100644 index 000000000..3318f95e2 --- /dev/null +++ b/Emby.Server.Implementations/Cryptography/X501Name.cs @@ -0,0 +1,393 @@ +// +// X501Name.cs: X.501 Distinguished Names stuff +// +// Author: +// Sebastien Pouliot <sebastien@ximian.com> +// +// (C) 2002, 2003 Motus Technologies Inc. (http://www.motus.com) +// Copyright (C) 2004-2006 Novell, Inc (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Globalization; +using System.Text; + +namespace Emby.Server.Core.Cryptography +{ + + // References: + // 1. Information technology - Open Systems Interconnection - The Directory: Models + // http://www.itu.int/rec/recommendation.asp?type=items&lang=e&parent=T-REC-X.501-200102-I + // 2. RFC2253: Lightweight Directory Access Protocol (v3): UTF-8 String Representation of Distinguished Names + // http://www.ietf.org/rfc/rfc2253.txt + + /* + * Name ::= CHOICE { RDNSequence } + * + * RDNSequence ::= SEQUENCE OF RelativeDistinguishedName + * + * RelativeDistinguishedName ::= SET OF AttributeTypeAndValue + */ + public sealed class X501 { + + static byte[] countryName = { 0x55, 0x04, 0x06 }; + static byte[] organizationName = { 0x55, 0x04, 0x0A }; + static byte[] organizationalUnitName = { 0x55, 0x04, 0x0B }; + static byte[] commonName = { 0x55, 0x04, 0x03 }; + static byte[] localityName = { 0x55, 0x04, 0x07 }; + static byte[] stateOrProvinceName = { 0x55, 0x04, 0x08 }; + static byte[] streetAddress = { 0x55, 0x04, 0x09 }; + //static byte[] serialNumber = { 0x55, 0x04, 0x05 }; + static byte[] domainComponent = { 0x09, 0x92, 0x26, 0x89, 0x93, 0xF2, 0x2C, 0x64, 0x01, 0x19 }; + static byte[] userid = { 0x09, 0x92, 0x26, 0x89, 0x93, 0xF2, 0x2C, 0x64, 0x01, 0x01 }; + static byte[] email = { 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x01 }; + static byte[] dnQualifier = { 0x55, 0x04, 0x2E }; + static byte[] title = { 0x55, 0x04, 0x0C }; + static byte[] surname = { 0x55, 0x04, 0x04 }; + static byte[] givenName = { 0x55, 0x04, 0x2A }; + static byte[] initial = { 0x55, 0x04, 0x2B }; + + private X501 () + { + } + + static public string ToString (ASN1 seq) + { + StringBuilder sb = new StringBuilder (); + for (int i = 0; i < seq.Count; i++) { + ASN1 entry = seq [i]; + AppendEntry (sb, entry, true); + + // separator (not on last iteration) + if (i < seq.Count - 1) + sb.Append (", "); + } + return sb.ToString (); + } + + static public string ToString (ASN1 seq, bool reversed, string separator, bool quotes) + { + StringBuilder sb = new StringBuilder (); + + if (reversed) { + for (int i = seq.Count - 1; i >= 0; i--) { + ASN1 entry = seq [i]; + AppendEntry (sb, entry, quotes); + + // separator (not on last iteration) + if (i > 0) + sb.Append (separator); + } + } else { + for (int i = 0; i < seq.Count; i++) { + ASN1 entry = seq [i]; + AppendEntry (sb, entry, quotes); + + // separator (not on last iteration) + if (i < seq.Count - 1) + sb.Append (separator); + } + } + return sb.ToString (); + } + + static private void AppendEntry (StringBuilder sb, ASN1 entry, bool quotes) + { + // multiple entries are valid + for (int k = 0; k < entry.Count; k++) { + ASN1 pair = entry [k]; + ASN1 s = pair [1]; + if (s == null) + continue; + + ASN1 poid = pair [0]; + if (poid == null) + continue; + + if (poid.CompareValue (countryName)) + sb.Append ("C="); + else if (poid.CompareValue (organizationName)) + sb.Append ("O="); + else if (poid.CompareValue (organizationalUnitName)) + sb.Append ("OU="); + else if (poid.CompareValue (commonName)) + sb.Append ("CN="); + else if (poid.CompareValue (localityName)) + sb.Append ("L="); + else if (poid.CompareValue (stateOrProvinceName)) + sb.Append ("S="); // NOTE: RFC2253 uses ST= + else if (poid.CompareValue (streetAddress)) + sb.Append ("STREET="); + else if (poid.CompareValue (domainComponent)) + sb.Append ("DC="); + else if (poid.CompareValue (userid)) + sb.Append ("UID="); + else if (poid.CompareValue (email)) + sb.Append ("E="); // NOTE: Not part of RFC2253 + else if (poid.CompareValue (dnQualifier)) + sb.Append ("dnQualifier="); + else if (poid.CompareValue (title)) + sb.Append ("T="); + else if (poid.CompareValue (surname)) + sb.Append ("SN="); + else if (poid.CompareValue (givenName)) + sb.Append ("G="); + else if (poid.CompareValue (initial)) + sb.Append ("I="); + else { + // unknown OID + sb.Append ("OID."); // NOTE: Not present as RFC2253 + sb.Append (ASN1Convert.ToOid (poid)); + sb.Append ("="); + } + + string sValue = null; + // 16bits or 8bits string ? TODO not complete (+special chars!) + if (s.Tag == 0x1E) { + // BMPSTRING + StringBuilder sb2 = new StringBuilder (); + for (int j = 1; j < s.Value.Length; j += 2) + sb2.Append ((char)s.Value[j]); + sValue = sb2.ToString (); + } else { + if (s.Tag == 0x14) + sValue = Encoding.UTF7.GetString (s.Value); + else + sValue = Encoding.UTF8.GetString (s.Value); + // in some cases we must quote (") the value + // Note: this doesn't seems to conform to RFC2253 + char[] specials = { ',', '+', '"', '\\', '<', '>', ';' }; + if (quotes) { + if ((sValue.IndexOfAny (specials, 0, sValue.Length) > 0) || + sValue.StartsWith (" ") || (sValue.EndsWith (" "))) + sValue = "\"" + sValue + "\""; + } + } + + sb.Append (sValue); + + // separator (not on last iteration) + if (k < entry.Count - 1) + sb.Append (", "); + } + } + + static private X520.AttributeTypeAndValue GetAttributeFromOid (string attributeType) + { + string s = attributeType.ToUpper (CultureInfo.InvariantCulture).Trim (); + switch (s) { + case "C": + return new X520.CountryName (); + case "O": + return new X520.OrganizationName (); + case "OU": + return new X520.OrganizationalUnitName (); + case "CN": + return new X520.CommonName (); + case "L": + return new X520.LocalityName (); + case "S": // Microsoft + case "ST": // RFC2253 + return new X520.StateOrProvinceName (); + case "E": // NOTE: Not part of RFC2253 + return new X520.EmailAddress (); + case "DC": // RFC2247 + return new X520.DomainComponent (); + case "UID": // RFC1274 + return new X520.UserId (); + case "DNQUALIFIER": + return new X520.DnQualifier (); + case "T": + return new X520.Title (); + case "SN": + return new X520.Surname (); + case "G": + return new X520.GivenName (); + case "I": + return new X520.Initial (); + default: + if (s.StartsWith ("OID.")) { + // MUST support it but it OID may be without it + return new X520.Oid (s.Substring (4)); + } else { + if (IsOid (s)) + return new X520.Oid (s); + else + return null; + } + } + } + + static private bool IsOid (string oid) + { + try { + ASN1 asn = ASN1Convert.FromOid (oid); + return (asn.Tag == 0x06); + } + catch { + return false; + } + } + + // no quote processing + static private X520.AttributeTypeAndValue ReadAttribute (string value, ref int pos) + { + while ((value[pos] == ' ') && (pos < value.Length)) + pos++; + + // get '=' position in substring + int equal = value.IndexOf ('=', pos); + if (equal == -1) { + string msg = ("No attribute found."); + throw new FormatException (msg); + } + + string s = value.Substring (pos, equal - pos); + X520.AttributeTypeAndValue atv = GetAttributeFromOid (s); + if (atv == null) { + string msg = ("Unknown attribute '{0}'."); + throw new FormatException (String.Format (msg, s)); + } + pos = equal + 1; // skip the '=' + return atv; + } + + static private bool IsHex (char c) + { + if (Char.IsDigit (c)) + return true; + char up = Char.ToUpper (c, CultureInfo.InvariantCulture); + return ((up >= 'A') && (up <= 'F')); + } + + static string ReadHex (string value, ref int pos) + { + StringBuilder sb = new StringBuilder (); + // it is (at least an) 8 bits char + sb.Append (value[pos++]); + sb.Append (value[pos]); + // look ahead for a 16 bits char + if ((pos < value.Length - 4) && (value[pos+1] == '\\') && IsHex (value[pos+2])) { + pos += 2; // pass last char and skip \ + sb.Append (value[pos++]); + sb.Append (value[pos]); + } + byte[] data = CryptoConvert.FromHex (sb.ToString ()); + return Encoding.UTF8.GetString (data); + } + + static private int ReadEscaped (StringBuilder sb, string value, int pos) + { + switch (value[pos]) { + case '\\': + case '"': + case '=': + case ';': + case '<': + case '>': + case '+': + case '#': + case ',': + sb.Append (value[pos]); + return pos; + default: + if (pos >= value.Length - 2) { + string msg = ("Malformed escaped value '{0}'."); + throw new FormatException (string.Format (msg, value.Substring (pos))); + } + // it's either a 8 bits or 16 bits char + sb.Append (ReadHex (value, ref pos)); + return pos; + } + } + + static private int ReadQuoted (StringBuilder sb, string value, int pos) + { + int original = pos; + while (pos <= value.Length) { + switch (value[pos]) { + case '"': + return pos; + case '\\': + return ReadEscaped (sb, value, pos); + default: + sb.Append (value[pos]); + pos++; + break; + } + } + string msg = ("Malformed quoted value '{0}'."); + throw new FormatException (string.Format (msg, value.Substring (original))); + } + + static private string ReadValue (string value, ref int pos) + { + int original = pos; + StringBuilder sb = new StringBuilder (); + while (pos < value.Length) { + switch (value [pos]) { + case '\\': + pos = ReadEscaped (sb, value, ++pos); + break; + case '"': + pos = ReadQuoted (sb, value, ++pos); + break; + case '=': + case ';': + case '<': + case '>': + string msg =("Malformed value '{0}' contains '{1}' outside quotes."); + throw new FormatException (string.Format (msg, value.Substring (original), value[pos])); + case '+': + case '#': + throw new NotImplementedException (); + case ',': + pos++; + return sb.ToString (); + default: + sb.Append (value[pos]); + break; + } + pos++; + } + return sb.ToString (); + } + + static public ASN1 FromString (string rdn) + { + if (rdn == null) + throw new ArgumentNullException ("rdn"); + + int pos = 0; + ASN1 asn1 = new ASN1 (0x30); + while (pos < rdn.Length) { + X520.AttributeTypeAndValue atv = ReadAttribute (rdn, ref pos); + atv.Value = ReadValue (rdn, ref pos); + + ASN1 sequence = new ASN1 (0x31); + sequence.Add (atv.GetASN1 ()); + asn1.Add (sequence); + } + return asn1; + } + } +} diff --git a/Emby.Server.Implementations/Cryptography/X509Builder.cs b/Emby.Server.Implementations/Cryptography/X509Builder.cs new file mode 100644 index 000000000..a2e292350 --- /dev/null +++ b/Emby.Server.Implementations/Cryptography/X509Builder.cs @@ -0,0 +1,153 @@ +// +// X509Builder.cs: Abstract builder class for X509 objects +// +// Author: +// Sebastien Pouliot <sebastien@ximian.com> +// +// (C) 2002, 2003 Motus Technologies Inc. (http://www.motus.com) +// (C) 2004 Novell (http://www.novell.com) +// + +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Globalization; +using System.Security.Cryptography; + +namespace Emby.Server.Core.Cryptography +{ + + public abstract class X509Builder { + + private const string defaultHash = "SHA1"; + private string hashName; + + protected X509Builder () + { + hashName = defaultHash; + } + + protected abstract ASN1 ToBeSigned (string hashName); + + // move to PKCS1 + protected string GetOid (string hashName) + { + switch (hashName.ToLower (CultureInfo.InvariantCulture)) { + case "md2": + // md2withRSAEncryption (1 2 840 113549 1 1 2) + return "1.2.840.113549.1.1.2"; + case "md4": + // md4withRSAEncryption (1 2 840 113549 1 1 3) + return "1.2.840.113549.1.1.3"; + case "md5": + // md5withRSAEncryption (1 2 840 113549 1 1 4) + return "1.2.840.113549.1.1.4"; + case "sha1": + // sha1withRSAEncryption (1 2 840 113549 1 1 5) + return "1.2.840.113549.1.1.5"; + case "sha256": + // sha256WithRSAEncryption OBJECT IDENTIFIER ::= { pkcs-1 11 } + return "1.2.840.113549.1.1.11"; + case "sha384": + // sha384WithRSAEncryption OBJECT IDENTIFIER ::= { pkcs-1 12 } + return "1.2.840.113549.1.1.12"; + case "sha512": + // sha512WithRSAEncryption OBJECT IDENTIFIER ::= { pkcs-1 13 } + return "1.2.840.113549.1.1.13"; + default: + throw new NotSupportedException ("Unknown hash algorithm " + hashName); + } + } + + public string Hash { + get { return hashName; } + set { + if (hashName == null) + hashName = defaultHash; + else + hashName = value; + } + } + + public virtual byte[] Sign (AsymmetricAlgorithm aa) + { + if (aa is RSA) + return Sign (aa as RSA); + else if (aa is DSA) + return Sign (aa as DSA); + else + throw new NotSupportedException ("Unknown Asymmetric Algorithm " + aa.ToString()); + } + + private byte[] Build (ASN1 tbs, string hashoid, byte[] signature) + { + ASN1 builder = new ASN1 (0x30); + builder.Add (tbs); + builder.Add (PKCS7.AlgorithmIdentifier (hashoid)); + // first byte of BITSTRING is the number of unused bits in the first byte + byte[] bitstring = new byte [signature.Length + 1]; + Buffer.BlockCopy (signature, 0, bitstring, 1, signature.Length); + builder.Add (new ASN1 (0x03, bitstring)); + return builder.GetBytes (); + } + + public virtual byte[] Sign (RSA key) + { + string oid = GetOid (hashName); + ASN1 tbs = ToBeSigned (oid); + HashAlgorithm ha = HashAlgorithm.Create (hashName); + byte[] hash = ha.ComputeHash (tbs.GetBytes ()); + + RSAPKCS1SignatureFormatter pkcs1 = new RSAPKCS1SignatureFormatter (key); + pkcs1.SetHashAlgorithm (hashName); + byte[] signature = pkcs1.CreateSignature (hash); + + return Build (tbs, oid, signature); + } + + public virtual byte[] Sign (DSA key) + { + string oid = "1.2.840.10040.4.3"; + ASN1 tbs = ToBeSigned (oid); + HashAlgorithm ha = HashAlgorithm.Create (hashName); + if (!(ha is SHA1)) + throw new NotSupportedException ("Only SHA-1 is supported for DSA"); + byte[] hash = ha.ComputeHash (tbs.GetBytes ()); + + DSASignatureFormatter dsa = new DSASignatureFormatter (key); + dsa.SetHashAlgorithm (hashName); + byte[] rs = dsa.CreateSignature (hash); + + // split R and S + byte[] r = new byte [20]; + Buffer.BlockCopy (rs, 0, r, 0, 20); + byte[] s = new byte [20]; + Buffer.BlockCopy (rs, 20, s, 0, 20); + ASN1 signature = new ASN1 (0x30); + signature.Add (new ASN1 (0x02, r)); + signature.Add (new ASN1 (0x02, s)); + + // dsaWithSha1 (1 2 840 10040 4 3) + return Build (tbs, oid, signature.GetBytes ()); + } + } +} diff --git a/Emby.Server.Implementations/Cryptography/X509Certificate.cs b/Emby.Server.Implementations/Cryptography/X509Certificate.cs new file mode 100644 index 000000000..3de58cfee --- /dev/null +++ b/Emby.Server.Implementations/Cryptography/X509Certificate.cs @@ -0,0 +1,563 @@ +// +// X509Certificates.cs: Handles X.509 certificates. +// +// Author: +// Sebastien Pouliot <sebastien@xamarin.com> +// +// (C) 2002, 2003 Motus Technologies Inc. (http://www.motus.com) +// Copyright (C) 2004-2006 Novell, Inc (http://www.novell.com) +// Copyright 2013 Xamarin Inc. (http://www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Runtime.Serialization; +using System.Security.Cryptography; +using System.Security.Permissions; +using System.Text; + +namespace Emby.Server.Core.Cryptography +{ + + // References: + // a. Internet X.509 Public Key Infrastructure Certificate and CRL Profile + // http://www.ietf.org/rfc/rfc3280.txt + // b. ITU ASN.1 standards (free download) + // http://www.itu.int/ITU-T/studygroups/com17/languages/ + + public class X509Certificate : ISerializable + { + + private ASN1 decoder; + + private byte[] m_encodedcert; + private DateTime m_from; + private DateTime m_until; + private ASN1 issuer; + private string m_issuername; + private string m_keyalgo; + private byte[] m_keyalgoparams; + private ASN1 subject; + private string m_subject; + private byte[] m_publickey; + private byte[] signature; + private string m_signaturealgo; + private byte[] m_signaturealgoparams; + private byte[] certhash; + private RSA _rsa; + private DSA _dsa; + + // from http://msdn.microsoft.com/en-gb/library/ff635835.aspx + private const string OID_DSA = "1.2.840.10040.4.1"; + private const string OID_RSA = "1.2.840.113549.1.1.1"; + + // from http://www.ietf.org/rfc/rfc2459.txt + // + //Certificate ::= SEQUENCE { + // tbsCertificate TBSCertificate, + // signatureAlgorithm AlgorithmIdentifier, + // signature BIT STRING } + // + //TBSCertificate ::= SEQUENCE { + // version [0] Version DEFAULT v1, + // serialNumber CertificateSerialNumber, + // signature AlgorithmIdentifier, + // issuer Name, + // validity Validity, + // subject Name, + // subjectPublicKeyInfo SubjectPublicKeyInfo, + // issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, + // -- If present, version shall be v2 or v3 + // subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, + // -- If present, version shall be v2 or v3 + // extensions [3] Extensions OPTIONAL + // -- If present, version shall be v3 -- } + private int version; + private byte[] serialnumber; + + private byte[] issuerUniqueID; + private byte[] subjectUniqueID; + private X509ExtensionCollection extensions; + + private static string encoding_error = ("Input data cannot be coded as a valid certificate."); + + + // that's were the real job is! + private void Parse (byte[] data) + { + try { + decoder = new ASN1 (data); + // Certificate + if (decoder.Tag != 0x30) + throw new CryptographicException (encoding_error); + // Certificate / TBSCertificate + if (decoder [0].Tag != 0x30) + throw new CryptographicException (encoding_error); + + ASN1 tbsCertificate = decoder [0]; + + int tbs = 0; + // Certificate / TBSCertificate / Version + ASN1 v = decoder [0][tbs]; + version = 1; // DEFAULT v1 + if ((v.Tag == 0xA0) && (v.Count > 0)) { + // version (optional) is present only in v2+ certs + version += v [0].Value [0]; // zero based + tbs++; + } + + // Certificate / TBSCertificate / CertificateSerialNumber + ASN1 sn = decoder [0][tbs++]; + if (sn.Tag != 0x02) + throw new CryptographicException (encoding_error); + serialnumber = sn.Value; + Array.Reverse (serialnumber, 0, serialnumber.Length); + + // Certificate / TBSCertificate / AlgorithmIdentifier + tbs++; + // ASN1 signatureAlgo = tbsCertificate.Element (tbs++, 0x30); + + issuer = tbsCertificate.Element (tbs++, 0x30); + m_issuername = X501.ToString (issuer); + + ASN1 validity = tbsCertificate.Element (tbs++, 0x30); + ASN1 notBefore = validity [0]; + m_from = ASN1Convert.ToDateTime (notBefore); + ASN1 notAfter = validity [1]; + m_until = ASN1Convert.ToDateTime (notAfter); + + subject = tbsCertificate.Element (tbs++, 0x30); + m_subject = X501.ToString (subject); + + ASN1 subjectPublicKeyInfo = tbsCertificate.Element (tbs++, 0x30); + + ASN1 algorithm = subjectPublicKeyInfo.Element (0, 0x30); + ASN1 algo = algorithm.Element (0, 0x06); + m_keyalgo = ASN1Convert.ToOid (algo); + // parameters ANY DEFINED BY algorithm OPTIONAL + // so we dont ask for a specific (Element) type and return DER + ASN1 parameters = algorithm [1]; + m_keyalgoparams = ((algorithm.Count > 1) ? parameters.GetBytes () : null); + + ASN1 subjectPublicKey = subjectPublicKeyInfo.Element (1, 0x03); + // we must drop th first byte (which is the number of unused bits + // in the BITSTRING) + int n = subjectPublicKey.Length - 1; + m_publickey = new byte [n]; + Buffer.BlockCopy (subjectPublicKey.Value, 1, m_publickey, 0, n); + + // signature processing + byte[] bitstring = decoder [2].Value; + // first byte contains unused bits in first byte + signature = new byte [bitstring.Length - 1]; + Buffer.BlockCopy (bitstring, 1, signature, 0, signature.Length); + + algorithm = decoder [1]; + algo = algorithm.Element (0, 0x06); + m_signaturealgo = ASN1Convert.ToOid (algo); + parameters = algorithm [1]; + if (parameters != null) + m_signaturealgoparams = parameters.GetBytes (); + else + m_signaturealgoparams = null; + + // Certificate / TBSCertificate / issuerUniqueID + ASN1 issuerUID = tbsCertificate.Element (tbs, 0x81); + if (issuerUID != null) { + tbs++; + issuerUniqueID = issuerUID.Value; + } + + // Certificate / TBSCertificate / subjectUniqueID + ASN1 subjectUID = tbsCertificate.Element (tbs, 0x82); + if (subjectUID != null) { + tbs++; + subjectUniqueID = subjectUID.Value; + } + + // Certificate / TBSCertificate / Extensions + ASN1 extns = tbsCertificate.Element (tbs, 0xA3); + if ((extns != null) && (extns.Count == 1)) + extensions = new X509ExtensionCollection (extns [0]); + else + extensions = new X509ExtensionCollection (null); + + // keep a copy of the original data + m_encodedcert = (byte[]) data.Clone (); + } + catch (Exception ex) { + throw new CryptographicException (encoding_error, ex); + } + } + + // constructors + + public X509Certificate (byte[] data) + { + if (data != null) { + // does it looks like PEM ? + if ((data.Length > 0) && (data [0] != 0x30)) { + try { + data = PEM ("CERTIFICATE", data); + } + catch (Exception ex) { + throw new CryptographicException (encoding_error, ex); + } + } + Parse (data); + } + } + + private byte[] GetUnsignedBigInteger (byte[] integer) + { + if (integer [0] == 0x00) { + // this first byte is added so we're sure it's an unsigned integer + // however we can't feed it into RSAParameters or DSAParameters + int length = integer.Length - 1; + byte[] uinteger = new byte [length]; + Buffer.BlockCopy (integer, 1, uinteger, 0, length); + return uinteger; + } + else + return integer; + } + + // public methods + + public DSA DSA { + get { + if (m_keyalgoparams == null) + throw new CryptographicException ("Missing key algorithm parameters."); + + if (_dsa == null && m_keyalgo == OID_DSA) { + DSAParameters dsaParams = new DSAParameters (); + // for DSA m_publickey contains 1 ASN.1 integer - Y + ASN1 pubkey = new ASN1 (m_publickey); + if ((pubkey == null) || (pubkey.Tag != 0x02)) + return null; + dsaParams.Y = GetUnsignedBigInteger (pubkey.Value); + + ASN1 param = new ASN1 (m_keyalgoparams); + if ((param == null) || (param.Tag != 0x30) || (param.Count < 3)) + return null; + if ((param [0].Tag != 0x02) || (param [1].Tag != 0x02) || (param [2].Tag != 0x02)) + return null; + dsaParams.P = GetUnsignedBigInteger (param [0].Value); + dsaParams.Q = GetUnsignedBigInteger (param [1].Value); + dsaParams.G = GetUnsignedBigInteger (param [2].Value); + + // BUG: MS BCL 1.0 can't import a key which + // isn't the same size as the one present in + // the container. + _dsa = (DSA) new DSACryptoServiceProvider (dsaParams.Y.Length << 3); + _dsa.ImportParameters (dsaParams); + } + return _dsa; + } + + set { + _dsa = value; + if (value != null) + _rsa = null; + } + } + + public X509ExtensionCollection Extensions { + get { return extensions; } + } + + public byte[] Hash { + get { + if (certhash == null) { + if ((decoder == null) || (decoder.Count < 1)) + return null; + string algo = PKCS1.HashNameFromOid (m_signaturealgo, false); + if (algo == null) + return null; + byte[] toBeSigned = decoder [0].GetBytes (); + using (var hash = PKCS1.CreateFromName (algo)) + certhash = hash.ComputeHash (toBeSigned, 0, toBeSigned.Length); + } + return (byte[]) certhash.Clone (); + } + } + + public virtual string IssuerName { + get { return m_issuername; } + } + + public virtual string KeyAlgorithm { + get { return m_keyalgo; } + } + + public virtual byte[] KeyAlgorithmParameters { + get { + if (m_keyalgoparams == null) + return null; + return (byte[]) m_keyalgoparams.Clone (); + } + set { m_keyalgoparams = value; } + } + + public virtual byte[] PublicKey { + get { + if (m_publickey == null) + return null; + return (byte[]) m_publickey.Clone (); + } + } + + public virtual RSA RSA { + get { + if (_rsa == null && m_keyalgo == OID_RSA) { + RSAParameters rsaParams = new RSAParameters (); + // for RSA m_publickey contains 2 ASN.1 integers + // the modulus and the public exponent + ASN1 pubkey = new ASN1 (m_publickey); + ASN1 modulus = pubkey [0]; + if ((modulus == null) || (modulus.Tag != 0x02)) + return null; + ASN1 exponent = pubkey [1]; + if (exponent.Tag != 0x02) + return null; + + rsaParams.Modulus = GetUnsignedBigInteger (modulus.Value); + rsaParams.Exponent = exponent.Value; + + // BUG: MS BCL 1.0 can't import a key which + // isn't the same size as the one present in + // the container. + int keySize = (rsaParams.Modulus.Length << 3); + _rsa = (RSA) new RSACryptoServiceProvider (keySize); + _rsa.ImportParameters (rsaParams); + } + return _rsa; + } + + set { + if (value != null) + _dsa = null; + _rsa = value; + } + } + + public virtual byte[] RawData { + get { + if (m_encodedcert == null) + return null; + return (byte[]) m_encodedcert.Clone (); + } + } + + public virtual byte[] SerialNumber { + get { + if (serialnumber == null) + return null; + return (byte[]) serialnumber.Clone (); + } + } + + public virtual byte[] Signature { + get { + if (signature == null) + return null; + + switch (m_signaturealgo) { + case "1.2.840.113549.1.1.2": // MD2 with RSA encryption + case "1.2.840.113549.1.1.3": // MD4 with RSA encryption + case "1.2.840.113549.1.1.4": // MD5 with RSA encryption + case "1.2.840.113549.1.1.5": // SHA-1 with RSA Encryption + case "1.3.14.3.2.29": // SHA1 with RSA signature + case "1.2.840.113549.1.1.11": // SHA-256 with RSA Encryption + case "1.2.840.113549.1.1.12": // SHA-384 with RSA Encryption + case "1.2.840.113549.1.1.13": // SHA-512 with RSA Encryption + case "1.3.36.3.3.1.2": // RIPEMD160 with RSA Encryption + return (byte[]) signature.Clone (); + + case "1.2.840.10040.4.3": // SHA-1 with DSA + ASN1 sign = new ASN1 (signature); + if ((sign == null) || (sign.Count != 2)) + return null; + byte[] part1 = sign [0].Value; + byte[] part2 = sign [1].Value; + byte[] sig = new byte [40]; + // parts may be less than 20 bytes (i.e. first bytes were 0x00) + // parts may be more than 20 bytes (i.e. first byte > 0x80, negative) + int s1 = System.Math.Max (0, part1.Length - 20); + int e1 = System.Math.Max (0, 20 - part1.Length); + Buffer.BlockCopy (part1, s1, sig, e1, part1.Length - s1); + int s2 = System.Math.Max (0, part2.Length - 20); + int e2 = System.Math.Max (20, 40 - part2.Length); + Buffer.BlockCopy (part2, s2, sig, e2, part2.Length - s2); + return sig; + + default: + throw new CryptographicException ("Unsupported hash algorithm: " + m_signaturealgo); + } + } + } + + public virtual string SignatureAlgorithm { + get { return m_signaturealgo; } + } + + public virtual byte[] SignatureAlgorithmParameters { + get { + if (m_signaturealgoparams == null) + return m_signaturealgoparams; + return (byte[]) m_signaturealgoparams.Clone (); + } + } + + public virtual string SubjectName { + get { return m_subject; } + } + + public virtual DateTime ValidFrom { + get { return m_from; } + } + + public virtual DateTime ValidUntil { + get { return m_until; } + } + + public int Version { + get { return version; } + } + + public bool IsCurrent { + get { return WasCurrent (DateTime.UtcNow); } + } + + public bool WasCurrent (DateTime instant) + { + return ((instant > ValidFrom) && (instant <= ValidUntil)); + } + + // uncommon v2 "extension" + public byte[] IssuerUniqueIdentifier { + get { + if (issuerUniqueID == null) + return null; + return (byte[]) issuerUniqueID.Clone (); + } + } + + // uncommon v2 "extension" + public byte[] SubjectUniqueIdentifier { + get { + if (subjectUniqueID == null) + return null; + return (byte[]) subjectUniqueID.Clone (); + } + } + + internal bool VerifySignature (DSA dsa) + { + // signatureOID is check by both this.Hash and this.Signature + DSASignatureDeformatter v = new DSASignatureDeformatter (dsa); + // only SHA-1 is supported + v.SetHashAlgorithm ("SHA1"); + return v.VerifySignature (this.Hash, this.Signature); + } + + internal bool VerifySignature (RSA rsa) + { + // SHA1-1 with DSA + if (m_signaturealgo == "1.2.840.10040.4.3") + return false; + RSAPKCS1SignatureDeformatter v = new RSAPKCS1SignatureDeformatter (rsa); + v.SetHashAlgorithm (PKCS1.HashNameFromOid (m_signaturealgo)); + return v.VerifySignature (this.Hash, this.Signature); + } + + public bool VerifySignature (AsymmetricAlgorithm aa) + { + if (aa == null) + throw new ArgumentNullException ("aa"); + + if (aa is RSA) + return VerifySignature (aa as RSA); + else if (aa is DSA) + return VerifySignature (aa as DSA); + else + throw new NotSupportedException ("Unknown Asymmetric Algorithm " + aa.ToString ()); + } + + public bool CheckSignature (byte[] hash, string hashAlgorithm, byte[] signature) + { + RSACryptoServiceProvider r = (RSACryptoServiceProvider) RSA; + return r.VerifyHash (hash, hashAlgorithm, signature); + } + + public bool IsSelfSigned { + get { + if (m_issuername != m_subject) + return false; + + try { + if (RSA != null) + return VerifySignature (RSA); + else if (DSA != null) + return VerifySignature (DSA); + else + return false; // e.g. a certificate with only DSA parameters + } + catch (CryptographicException) { + return false; + } + } + } + + public ASN1 GetIssuerName () + { + return issuer; + } + + public ASN1 GetSubjectName () + { + return subject; + } + + protected X509Certificate (SerializationInfo info, StreamingContext context) + { + Parse ((byte[]) info.GetValue ("raw", typeof (byte[]))); + } + + [SecurityPermission (SecurityAction.Demand, SerializationFormatter = true)] + public virtual void GetObjectData (SerializationInfo info, StreamingContext context) + { + info.AddValue ("raw", m_encodedcert); + // note: we NEVER serialize the private key + } + + static byte[] PEM (string type, byte[] data) + { + string pem = Encoding.ASCII.GetString (data); + string header = String.Format ("-----BEGIN {0}-----", type); + string footer = String.Format ("-----END {0}-----", type); + int start = pem.IndexOf (header) + header.Length; + int end = pem.IndexOf (footer, start); + string base64 = pem.Substring (start, (end - start)); + return Convert.FromBase64String (base64); + } + } +} diff --git a/Emby.Server.Implementations/Cryptography/X509CertificateBuilder.cs b/Emby.Server.Implementations/Cryptography/X509CertificateBuilder.cs new file mode 100644 index 000000000..cecff6578 --- /dev/null +++ b/Emby.Server.Implementations/Cryptography/X509CertificateBuilder.cs @@ -0,0 +1,245 @@ +// +// X509CertificateBuilder.cs: Handles building of X.509 certificates. +// +// Author: +// Sebastien Pouliot <sebastien@ximian.com> +// +// (C) 2003 Motus Technologies Inc. (http://www.motus.com) +// (C) 2004 Novell (http://www.novell.com) +// + +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Security.Cryptography; + +namespace Emby.Server.Core.Cryptography +{ + // From RFC3280 + /* + * Certificate ::= SEQUENCE { + * tbsCertificate TBSCertificate, + * signatureAlgorithm AlgorithmIdentifier, + * signature BIT STRING + * } + * TBSCertificate ::= SEQUENCE { + * version [0] Version DEFAULT v1, + * serialNumber CertificateSerialNumber, + * signature AlgorithmIdentifier, + * issuer Name, + * validity Validity, + * subject Name, + * subjectPublicKeyInfo SubjectPublicKeyInfo, + * issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, + * -- If present, version MUST be v2 or v3 + * subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, + * -- If present, version MUST be v2 or v3 + * extensions [3] Extensions OPTIONAL + * -- If present, version MUST be v3 -- + * } + * Version ::= INTEGER { v1(0), v2(1), v3(2) } + * CertificateSerialNumber ::= INTEGER + * Validity ::= SEQUENCE { + * notBefore Time, + * notAfter Time + * } + * Time ::= CHOICE { + * utcTime UTCTime, + * generalTime GeneralizedTime + * } + */ + public class X509CertificateBuilder : X509Builder { + + private byte version; + private byte[] sn; + private string issuer; + private DateTime notBefore; + private DateTime notAfter; + private string subject; + private AsymmetricAlgorithm aa; + private byte[] issuerUniqueID; + private byte[] subjectUniqueID; + private X509ExtensionCollection extensions; + + public X509CertificateBuilder () : this (3) {} + + public X509CertificateBuilder (byte version) + { + if (version > 3) + throw new ArgumentException ("Invalid certificate version"); + this.version = version; + extensions = new X509ExtensionCollection (); + } + + public byte Version { + get { return version; } + set { version = value; } + } + + public byte[] SerialNumber { + get { return sn; } + set { sn = value; } + } + + public string IssuerName { + get { return issuer; } + set { issuer = value; } + } + + public DateTime NotBefore { + get { return notBefore; } + set { notBefore = value; } + } + + public DateTime NotAfter { + get { return notAfter; } + set { notAfter = value; } + } + + public string SubjectName { + get { return subject; } + set { subject = value; } + } + + public AsymmetricAlgorithm SubjectPublicKey { + get { return aa; } + set { aa = value; } + } + + public byte[] IssuerUniqueId { + get { return issuerUniqueID; } + set { issuerUniqueID = value; } + } + + public byte[] SubjectUniqueId { + get { return subjectUniqueID; } + set { subjectUniqueID = value; } + } + + public X509ExtensionCollection Extensions { + get { return extensions; } + } + + + /* SubjectPublicKeyInfo ::= SEQUENCE { + * algorithm AlgorithmIdentifier, + * subjectPublicKey BIT STRING } + */ + private ASN1 SubjectPublicKeyInfo () + { + ASN1 keyInfo = new ASN1 (0x30); + if (aa is RSA) { + keyInfo.Add (PKCS7.AlgorithmIdentifier ("1.2.840.113549.1.1.1")); + RSAParameters p = (aa as RSA).ExportParameters (false); + /* RSAPublicKey ::= SEQUENCE { + * modulus INTEGER, -- n + * publicExponent INTEGER } -- e + */ + ASN1 key = new ASN1 (0x30); + key.Add (ASN1Convert.FromUnsignedBigInteger (p.Modulus)); + key.Add (ASN1Convert.FromUnsignedBigInteger (p.Exponent)); + keyInfo.Add (new ASN1 (UniqueIdentifier (key.GetBytes ()))); + } + else if (aa is DSA) { + DSAParameters p = (aa as DSA).ExportParameters (false); + /* Dss-Parms ::= SEQUENCE { + * p INTEGER, + * q INTEGER, + * g INTEGER } + */ + ASN1 param = new ASN1 (0x30); + param.Add (ASN1Convert.FromUnsignedBigInteger (p.P)); + param.Add (ASN1Convert.FromUnsignedBigInteger (p.Q)); + param.Add (ASN1Convert.FromUnsignedBigInteger (p.G)); + keyInfo.Add (PKCS7.AlgorithmIdentifier ("1.2.840.10040.4.1", param)); + ASN1 key = keyInfo.Add (new ASN1 (0x03)); + // DSAPublicKey ::= INTEGER -- public key, y + key.Add (ASN1Convert.FromUnsignedBigInteger (p.Y)); + } + else + throw new NotSupportedException ("Unknown Asymmetric Algorithm " + aa.ToString ()); + return keyInfo; + } + + private byte[] UniqueIdentifier (byte[] id) + { + // UniqueIdentifier ::= BIT STRING + ASN1 uid = new ASN1 (0x03); + // first byte in a BITSTRING is the number of unused bits in the first byte + byte[] v = new byte [id.Length + 1]; + Buffer.BlockCopy (id, 0, v, 1, id.Length); + uid.Value = v; + return uid.GetBytes (); + } + + protected override ASN1 ToBeSigned (string oid) + { + // TBSCertificate + ASN1 tbsCert = new ASN1 (0x30); + + if (version > 1) { + // TBSCertificate / [0] Version DEFAULT v1, + byte[] ver = { (byte)(version - 1) }; + ASN1 v = tbsCert.Add (new ASN1 (0xA0)); + v.Add (new ASN1 (0x02, ver)); + } + + // TBSCertificate / CertificateSerialNumber, + tbsCert.Add (new ASN1 (0x02, sn)); + + // TBSCertificate / AlgorithmIdentifier, + tbsCert.Add (PKCS7.AlgorithmIdentifier (oid)); + + // TBSCertificate / Name + tbsCert.Add (X501.FromString (issuer)); + + // TBSCertificate / Validity + ASN1 validity = tbsCert.Add (new ASN1 (0x30)); + // TBSCertificate / Validity / Time + validity.Add (ASN1Convert.FromDateTime (notBefore)); + // TBSCertificate / Validity / Time + validity.Add (ASN1Convert.FromDateTime (notAfter)); + + // TBSCertificate / Name + tbsCert.Add (X501.FromString (subject)); + + // TBSCertificate / SubjectPublicKeyInfo + tbsCert.Add (SubjectPublicKeyInfo ()); + + if (version > 1) { + // TBSCertificate / [1] IMPLICIT UniqueIdentifier OPTIONAL + if (issuerUniqueID != null) + tbsCert.Add (new ASN1 (0xA1, UniqueIdentifier (issuerUniqueID))); + + // TBSCertificate / [2] IMPLICIT UniqueIdentifier OPTIONAL + if (subjectUniqueID != null) + tbsCert.Add (new ASN1 (0xA1, UniqueIdentifier (subjectUniqueID))); + + // TBSCertificate / [3] Extensions OPTIONAL + if ((version > 2) && (extensions.Count > 0)) + tbsCert.Add (new ASN1 (0xA3, extensions.GetBytes ())); + } + + return tbsCert; + } + } +} diff --git a/Emby.Server.Implementations/Cryptography/X509CertificateCollection.cs b/Emby.Server.Implementations/Cryptography/X509CertificateCollection.cs new file mode 100644 index 000000000..a129bfc1a --- /dev/null +++ b/Emby.Server.Implementations/Cryptography/X509CertificateCollection.cs @@ -0,0 +1,201 @@ +// +// Based on System.Security.Cryptography.X509Certificates.X509CertificateCollection +// in System assembly +// +// Authors: +// Lawrence Pit (loz@cable.a2000.nl) +// Sebastien Pouliot <sebastien@ximian.com> +// + +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections; + +namespace Emby.Server.Core.Cryptography +{ + + [Serializable] + public class X509CertificateCollection : CollectionBase, IEnumerable { + + public X509CertificateCollection () + { + } + + public X509CertificateCollection (X509Certificate [] value) + { + AddRange (value); + } + + public X509CertificateCollection (X509CertificateCollection value) + { + AddRange (value); + } + + // Properties + + public X509Certificate this [int index] { + get { return (X509Certificate) InnerList [index]; } + set { InnerList [index] = value; } + } + + // Methods + + public int Add (X509Certificate value) + { + if (value == null) + throw new ArgumentNullException ("value"); + + return InnerList.Add (value); + } + + public void AddRange (X509Certificate [] value) + { + if (value == null) + throw new ArgumentNullException ("value"); + + for (int i = 0; i < value.Length; i++) + InnerList.Add (value [i]); + } + + public void AddRange (X509CertificateCollection value) + { + if (value == null) + throw new ArgumentNullException ("value"); + + for (int i = 0; i < value.InnerList.Count; i++) + InnerList.Add (value [i]); + } + + public bool Contains (X509Certificate value) + { + return (IndexOf (value) != -1); + } + + public void CopyTo (X509Certificate[] array, int index) + { + InnerList.CopyTo (array, index); + } + + public new X509CertificateEnumerator GetEnumerator () + { + return new X509CertificateEnumerator (this); + } + + IEnumerator IEnumerable.GetEnumerator () + { + return InnerList.GetEnumerator (); + } + + public override int GetHashCode () + { + return InnerList.GetHashCode (); + } + + public int IndexOf (X509Certificate value) + { + if (value == null) + throw new ArgumentNullException ("value"); + + byte[] hash = value.Hash; + for (int i=0; i < InnerList.Count; i++) { + X509Certificate x509 = (X509Certificate) InnerList [i]; + if (Compare (x509.Hash, hash)) + return i; + } + return -1; + } + + public void Insert (int index, X509Certificate value) + { + InnerList.Insert (index, value); + } + + public void Remove (X509Certificate value) + { + InnerList.Remove (value); + } + + // private stuff + + private bool Compare (byte[] array1, byte[] array2) + { + if ((array1 == null) && (array2 == null)) + return true; + if ((array1 == null) || (array2 == null)) + return false; + if (array1.Length != array2.Length) + return false; + for (int i=0; i < array1.Length; i++) { + if (array1 [i] != array2 [i]) + return false; + } + return true; + } + + // Inner Class + + public class X509CertificateEnumerator : IEnumerator { + + private IEnumerator enumerator; + + // Constructors + + public X509CertificateEnumerator (X509CertificateCollection mappings) + { + enumerator = ((IEnumerable) mappings).GetEnumerator (); + } + + // Properties + + public X509Certificate Current { + get { return (X509Certificate) enumerator.Current; } + } + + object IEnumerator.Current { + get { return enumerator.Current; } + } + + // Methods + + bool IEnumerator.MoveNext () + { + return enumerator.MoveNext (); + } + + void IEnumerator.Reset () + { + enumerator.Reset (); + } + + public bool MoveNext () + { + return enumerator.MoveNext (); + } + + public void Reset () + { + enumerator.Reset (); + } + } + } +} diff --git a/Emby.Server.Implementations/Cryptography/X509Extension.cs b/Emby.Server.Implementations/Cryptography/X509Extension.cs new file mode 100644 index 000000000..36b17deba --- /dev/null +++ b/Emby.Server.Implementations/Cryptography/X509Extension.cs @@ -0,0 +1,208 @@ +// +// X509Extension.cs: Base class for all X.509 extensions. +// +// Author: +// Sebastien Pouliot <sebastien@ximian.com> +// +// (C) 2003 Motus Technologies Inc. (http://www.motus.com) +// Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Globalization; +using System.Text; + +namespace Emby.Server.Core.Cryptography +{ + /* + * Extension ::= SEQUENCE { + * extnID OBJECT IDENTIFIER, + * critical BOOLEAN DEFAULT FALSE, + * extnValue OCTET STRING + * } + */ + public class X509Extension { + + protected string extnOid; + protected bool extnCritical; + protected ASN1 extnValue; + + protected X509Extension () + { + extnCritical = false; + } + + public X509Extension (ASN1 asn1) + { + if ((asn1.Tag != 0x30) || (asn1.Count < 2)) + throw new ArgumentException (("Invalid X.509 extension.")); + if (asn1[0].Tag != 0x06) + throw new ArgumentException (("Invalid X.509 extension.")); + + extnOid = ASN1Convert.ToOid (asn1[0]); + extnCritical = ((asn1[1].Tag == 0x01) && (asn1[1].Value[0] == 0xFF)); + // last element is an octet string which may need to be decoded + extnValue = asn1 [asn1.Count - 1]; + if ((extnValue.Tag == 0x04) && (extnValue.Length > 0) && (extnValue.Count == 0)) { + try { + ASN1 encapsulated = new ASN1 (extnValue.Value); + extnValue.Value = null; + extnValue.Add (encapsulated); + } + catch { + // data isn't ASN.1 + } + } + Decode (); + } + + public X509Extension (X509Extension extension) + { + if (extension == null) + throw new ArgumentNullException ("extension"); + if ((extension.Value == null) || (extension.Value.Tag != 0x04) || (extension.Value.Count != 1)) + throw new ArgumentException (("Invalid X.509 extension.")); + + extnOid = extension.Oid; + extnCritical = extension.Critical; + extnValue = extension.Value; + Decode (); + } + + // encode the extension *into* an OCTET STRING + protected virtual void Decode () + { + } + + // decode the extension from *inside* an OCTET STRING + protected virtual void Encode () + { + } + + public ASN1 ASN1 { + get { + ASN1 extension = new ASN1 (0x30); + extension.Add (ASN1Convert.FromOid (extnOid)); + if (extnCritical) + extension.Add (new ASN1 (0x01, new byte [1] { 0xFF })); + Encode (); + extension.Add (extnValue); + return extension; + } + } + + public string Oid { + get { return extnOid; } + } + + public bool Critical { + get { return extnCritical; } + set { extnCritical = value; } + } + + // this gets overrided with more meaningful names + public virtual string Name { + get { return extnOid; } + } + + public ASN1 Value { + get { + if (extnValue == null) { + Encode (); + } + return extnValue; + } + } + + public override bool Equals (object obj) + { + if (obj == null) + return false; + + X509Extension ex = (obj as X509Extension); + if (ex == null) + return false; + + if (extnCritical != ex.extnCritical) + return false; + if (extnOid != ex.extnOid) + return false; + if (extnValue.Length != ex.extnValue.Length) + return false; + + for (int i=0; i < extnValue.Length; i++) { + if (extnValue [i] != ex.extnValue [i]) + return false; + } + return true; + } + + public byte[] GetBytes () + { + return ASN1.GetBytes (); + } + + public override int GetHashCode () + { + // OID should be unique in a collection of extensions + return extnOid.GetHashCode (); + } + + private void WriteLine (StringBuilder sb, int n, int pos) + { + byte[] value = extnValue.Value; + int p = pos; + for (int j=0; j < 8; j++) { + if (j < n) { + sb.Append (value [p++].ToString ("X2", CultureInfo.InvariantCulture)); + sb.Append (" "); + } + else + sb.Append (" "); + } + sb.Append (" "); + p = pos; + for (int j=0; j < n; j++) { + byte b = value [p++]; + if (b < 0x20) + sb.Append ("."); + else + sb.Append (Convert.ToChar (b)); + } + sb.Append (Environment.NewLine); + } + + public override string ToString () + { + StringBuilder sb = new StringBuilder (); + int div = (extnValue.Length >> 3); + int rem = (extnValue.Length - (div << 3)); + int x = 0; + for (int i=0; i < div; i++) { + WriteLine (sb, 8, x); + x += 8; + } + WriteLine (sb, rem, x); + return sb.ToString (); + } + } +} diff --git a/Emby.Server.Implementations/Cryptography/X509Extensions.cs b/Emby.Server.Implementations/Cryptography/X509Extensions.cs new file mode 100644 index 000000000..018a04d79 --- /dev/null +++ b/Emby.Server.Implementations/Cryptography/X509Extensions.cs @@ -0,0 +1,195 @@ +// +// X509Extensions.cs: Handles X.509 extensions. +// +// Author: +// Sebastien Pouliot <sebastien@ximian.com> +// +// (C) 2003 Motus Technologies Inc. (http://www.motus.com) +// (C) 2004 Novell (http://www.novell.com) +// + +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections; + +namespace Emby.Server.Core.Cryptography +{ + /* + * Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension + * + * Note: 1..MAX -> There shouldn't be 0 Extensions in the ASN1 structure + */ + public sealed class X509ExtensionCollection : CollectionBase, IEnumerable { + + private bool readOnly; + + public X509ExtensionCollection () : base () + { + } + + public X509ExtensionCollection (ASN1 asn1) : this () + { + readOnly = true; + if (asn1 == null) + return; + if (asn1.Tag != 0x30) + throw new Exception ("Invalid extensions format"); + for (int i=0; i < asn1.Count; i++) { + X509Extension extension = new X509Extension (asn1 [i]); + InnerList.Add (extension); + } + } + + public int Add (X509Extension extension) + { + if (extension == null) + throw new ArgumentNullException ("extension"); + if (readOnly) + throw new NotSupportedException ("Extensions are read only"); + + return InnerList.Add (extension); + } + + public void AddRange (X509Extension[] extension) + { + if (extension == null) + throw new ArgumentNullException ("extension"); + if (readOnly) + throw new NotSupportedException ("Extensions are read only"); + + for (int i = 0; i < extension.Length; i++) + InnerList.Add (extension [i]); + } + + public void AddRange (X509ExtensionCollection collection) + { + if (collection == null) + throw new ArgumentNullException ("collection"); + if (readOnly) + throw new NotSupportedException ("Extensions are read only"); + + for (int i = 0; i < collection.InnerList.Count; i++) + InnerList.Add (collection [i]); + } + + public bool Contains (X509Extension extension) + { + return (IndexOf (extension) != -1); + } + + public bool Contains (string oid) + { + return (IndexOf (oid) != -1); + } + + public void CopyTo (X509Extension[] extensions, int index) + { + if (extensions == null) + throw new ArgumentNullException ("extensions"); + + InnerList.CopyTo (extensions, index); + } + + public int IndexOf (X509Extension extension) + { + if (extension == null) + throw new ArgumentNullException ("extension"); + + for (int i=0; i < InnerList.Count; i++) { + X509Extension ex = (X509Extension) InnerList [i]; + if (ex.Equals (extension)) + return i; + } + return -1; + } + + public int IndexOf (string oid) + { + if (oid == null) + throw new ArgumentNullException ("oid"); + + for (int i=0; i < InnerList.Count; i++) { + X509Extension ex = (X509Extension) InnerList [i]; + if (ex.Oid == oid) + return i; + } + return -1; + } + + public void Insert (int index, X509Extension extension) + { + if (extension == null) + throw new ArgumentNullException ("extension"); + + InnerList.Insert (index, extension); + } + + public void Remove (X509Extension extension) + { + if (extension == null) + throw new ArgumentNullException ("extension"); + + InnerList.Remove (extension); + } + + public void Remove (string oid) + { + if (oid == null) + throw new ArgumentNullException ("oid"); + + int index = IndexOf (oid); + if (index != -1) + InnerList.RemoveAt (index); + } + + IEnumerator IEnumerable.GetEnumerator () + { + return InnerList.GetEnumerator (); + } + + public X509Extension this [int index] { + get { return (X509Extension) InnerList [index]; } + } + + public X509Extension this [string oid] { + get { + int index = IndexOf (oid); + if (index == -1) + return null; + return (X509Extension) InnerList [index]; + } + } + + public byte[] GetBytes () + { + if (InnerList.Count < 1) + return null; + ASN1 sequence = new ASN1 (0x30); + for (int i=0; i < InnerList.Count; i++) { + X509Extension x = (X509Extension) InnerList [i]; + sequence.Add (x.ASN1); + } + return sequence.GetBytes (); + } + } +} diff --git a/Emby.Server.Implementations/Cryptography/X520Attributes.cs b/Emby.Server.Implementations/Cryptography/X520Attributes.cs new file mode 100644 index 000000000..da7fd2b82 --- /dev/null +++ b/Emby.Server.Implementations/Cryptography/X520Attributes.cs @@ -0,0 +1,346 @@ +// +// X520.cs: X.520 related stuff (attributes, RDN) +// +// Author: +// Sebastien Pouliot <sebastien@ximian.com> +// +// (C) 2002, 2003 Motus Technologies Inc. (http://www.motus.com) +// Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Text; + +namespace Emby.Server.Core.Cryptography +{ + + // References: + // 1. Information technology - Open Systems Interconnection - The Directory: Selected attribute types + // http://www.itu.int/rec/recommendation.asp?type=folders&lang=e&parent=T-REC-X.520 + // 2. Internet X.509 Public Key Infrastructure Certificate and CRL Profile + // http://www.ietf.org/rfc/rfc3280.txt + // 3. A Summary of the X.500(96) User Schema for use with LDAPv3 + // http://www.faqs.org/rfcs/rfc2256.html + // 4. RFC 2247 - Using Domains in LDAP/X.500 Distinguished Names + // http://www.faqs.org/rfcs/rfc2247.html + + /* + * AttributeTypeAndValue ::= SEQUENCE { + * type AttributeType, + * value AttributeValue + * } + * + * AttributeType ::= OBJECT IDENTIFIER + * + * AttributeValue ::= ANY DEFINED BY AttributeType + */ + public class X520 { + + public abstract class AttributeTypeAndValue { + private string oid; + private string attrValue; + private int upperBound; + private byte encoding; + + protected AttributeTypeAndValue (string oid, int upperBound) + { + this.oid = oid; + this.upperBound = upperBound; + this.encoding = 0xFF; + } + + protected AttributeTypeAndValue (string oid, int upperBound, byte encoding) + { + this.oid = oid; + this.upperBound = upperBound; + this.encoding = encoding; + } + + public string Value { + get { return attrValue; } + set { + if ((attrValue != null) && (attrValue.Length > upperBound)) { + string msg = ("Value length bigger than upperbound ({0})."); + throw new FormatException (String.Format (msg, upperBound)); + } + attrValue = value; + } + } + + public ASN1 ASN1 { + get { return GetASN1 (); } + } + + internal ASN1 GetASN1 (byte encoding) + { + byte encode = encoding; + if (encode == 0xFF) + encode = SelectBestEncoding (); + + ASN1 asn1 = new ASN1 (0x30); + asn1.Add (ASN1Convert.FromOid (oid)); + switch (encode) { + case 0x13: + // PRINTABLESTRING + asn1.Add (new ASN1 (0x13, Encoding.ASCII.GetBytes (attrValue))); + break; + case 0x16: + // IA5STRING + asn1.Add (new ASN1 (0x16, Encoding.ASCII.GetBytes (attrValue))); + break; + case 0x1E: + // BMPSTRING + asn1.Add (new ASN1 (0x1E, Encoding.BigEndianUnicode.GetBytes (attrValue))); + break; + } + return asn1; + } + + internal ASN1 GetASN1 () + { + return GetASN1 (encoding); + } + + public byte[] GetBytes (byte encoding) + { + return GetASN1 (encoding) .GetBytes (); + } + + public byte[] GetBytes () + { + return GetASN1 () .GetBytes (); + } + + private byte SelectBestEncoding () + { + foreach (char c in attrValue) { + switch (c) { + case '@': + case '_': + return 0x1E; // BMPSTRING + default: + if (c > 127) + return 0x1E; // BMPSTRING + break; + } + } + return 0x13; // PRINTABLESTRING + } + } + + public class Name : AttributeTypeAndValue { + + public Name () : base ("2.5.4.41", 32768) + { + } + } + + public class CommonName : AttributeTypeAndValue { + + public CommonName () : base ("2.5.4.3", 64) + { + } + } + + // RFC2256, Section 5.6 + public class SerialNumber : AttributeTypeAndValue { + + // max length 64 bytes, Printable String only + public SerialNumber () + : base ("2.5.4.5", 64, 0x13) + { + } + } + + public class LocalityName : AttributeTypeAndValue { + + public LocalityName () : base ("2.5.4.7", 128) + { + } + } + + public class StateOrProvinceName : AttributeTypeAndValue { + + public StateOrProvinceName () : base ("2.5.4.8", 128) + { + } + } + + public class OrganizationName : AttributeTypeAndValue { + + public OrganizationName () : base ("2.5.4.10", 64) + { + } + } + + public class OrganizationalUnitName : AttributeTypeAndValue { + + public OrganizationalUnitName () : base ("2.5.4.11", 64) + { + } + } + + // NOTE: Not part of RFC2253 + public class EmailAddress : AttributeTypeAndValue { + + public EmailAddress () : base ("1.2.840.113549.1.9.1", 128, 0x16) + { + } + } + + // RFC2247, Section 4 + public class DomainComponent : AttributeTypeAndValue { + + // no maximum length defined + public DomainComponent () + : base ("0.9.2342.19200300.100.1.25", Int32.MaxValue, 0x16) + { + } + } + + // RFC1274, Section 9.3.1 + public class UserId : AttributeTypeAndValue { + + public UserId () + : base ("0.9.2342.19200300.100.1.1", 256) + { + } + } + + public class Oid : AttributeTypeAndValue { + + public Oid (string oid) + : base (oid, Int32.MaxValue) + { + } + } + + /* -- Naming attributes of type X520Title + * id-at-title AttributeType ::= { id-at 12 } + * + * X520Title ::= CHOICE { + * teletexString TeletexString (SIZE (1..ub-title)), + * printableString PrintableString (SIZE (1..ub-title)), + * universalString UniversalString (SIZE (1..ub-title)), + * utf8String UTF8String (SIZE (1..ub-title)), + * bmpString BMPString (SIZE (1..ub-title)) + * } + */ + public class Title : AttributeTypeAndValue { + + public Title () : base ("2.5.4.12", 64) + { + } + } + + public class CountryName : AttributeTypeAndValue { + + // (0x13) PRINTABLESTRING + public CountryName () : base ("2.5.4.6", 2, 0x13) + { + } + } + + public class DnQualifier : AttributeTypeAndValue { + + // (0x13) PRINTABLESTRING + public DnQualifier () : base ("2.5.4.46", 2, 0x13) + { + } + } + + public class Surname : AttributeTypeAndValue { + + public Surname () : base ("2.5.4.4", 32768) + { + } + } + + public class GivenName : AttributeTypeAndValue { + + public GivenName () : base ("2.5.4.42", 16) + { + } + } + + public class Initial : AttributeTypeAndValue { + + public Initial () : base ("2.5.4.43", 5) + { + } + } + + } + + /* From RFC3280 + * -- specifications of Upper Bounds MUST be regarded as mandatory + * -- from Annex B of ITU-T X.411 Reference Definition of MTS Parameter + * + * -- Upper Bounds + * + * ub-name INTEGER ::= 32768 + * ub-common-name INTEGER ::= 64 + * ub-locality-name INTEGER ::= 128 + * ub-state-name INTEGER ::= 128 + * ub-organization-name INTEGER ::= 64 + * ub-organizational-unit-name INTEGER ::= 64 + * ub-title INTEGER ::= 64 + * ub-serial-number INTEGER ::= 64 + * ub-match INTEGER ::= 128 + * ub-emailaddress-length INTEGER ::= 128 + * ub-common-name-length INTEGER ::= 64 + * ub-country-name-alpha-length INTEGER ::= 2 + * ub-country-name-numeric-length INTEGER ::= 3 + * ub-domain-defined-attributes INTEGER ::= 4 + * ub-domain-defined-attribute-type-length INTEGER ::= 8 + * ub-domain-defined-attribute-value-length INTEGER ::= 128 + * ub-domain-name-length INTEGER ::= 16 + * ub-extension-attributes INTEGER ::= 256 + * ub-e163-4-number-length INTEGER ::= 15 + * ub-e163-4-sub-address-length INTEGER ::= 40 + * ub-generation-qualifier-length INTEGER ::= 3 + * ub-given-name-length INTEGER ::= 16 + * ub-initials-length INTEGER ::= 5 + * ub-integer-options INTEGER ::= 256 + * ub-numeric-user-id-length INTEGER ::= 32 + * ub-organization-name-length INTEGER ::= 64 + * ub-organizational-unit-name-length INTEGER ::= 32 + * ub-organizational-units INTEGER ::= 4 + * ub-pds-name-length INTEGER ::= 16 + * ub-pds-parameter-length INTEGER ::= 30 + * ub-pds-physical-address-lines INTEGER ::= 6 + * ub-postal-code-length INTEGER ::= 16 + * ub-pseudonym INTEGER ::= 128 + * ub-surname-length INTEGER ::= 40 + * ub-terminal-id-length INTEGER ::= 24 + * ub-unformatted-address-length INTEGER ::= 180 + * ub-x121-address-length INTEGER ::= 16 + * + * -- Note - upper bounds on string types, such as TeletexString, are + * -- measured in characters. Excepting PrintableString or IA5String, a + * -- significantly greater number of octets will be required to hold + * -- such a value. As a minimum, 16 octets, or twice the specified + * -- upper bound, whichever is the larger, should be allowed for + * -- TeletexString. For UTF8String or UniversalString at least four + * -- times the upper bound should be allowed. + */ +} diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 462ff3e47..a34c90cb4 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -131,25 +131,11 @@ namespace Emby.Server.Implementations.Data { queries.Add("PRAGMA temp_store = memory"); } - - ////foreach (var query in queries) - ////{ - //// db.Execute(query); - ////} - - //Logger.Info("synchronous: " + db.Query("PRAGMA synchronous").SelectScalarString().First()); - //Logger.Info("temp_store: " + db.Query("PRAGMA temp_store").SelectScalarString().First()); - - /*if (!string.Equals(_defaultWal, "wal", StringComparison.OrdinalIgnoreCase)) + else { - queries.Add("PRAGMA journal_mode=WAL"); - - using (WriteLock.Write()) - { - db.ExecuteAll(string.Join(";", queries.ToArray())); - } + queries.Add("PRAGMA temp_store = file"); } - else*/ + foreach (var query in queries) { db.Execute(query); @@ -208,6 +194,13 @@ namespace Emby.Server.Implementations.Data "pragma temp_store = memory" }); } + else + { + queries.AddRange(new List<string> + { + "pragma temp_store = file" + }); + } db.ExecuteAll(string.Join(";", queries.ToArray())); Logger.Info("PRAGMA synchronous=" + db.Query("PRAGMA synchronous").SelectScalarString().First()); diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index f43e45441..37ba2eb5f 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -11,9 +11,6 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.IO; -using MediaBrowser.Controller.Channels; -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Model.Tasks; namespace Emby.Server.Implementations.Data { @@ -34,31 +31,9 @@ namespace Emby.Server.Implementations.Data _appPaths = appPaths; } - public string Name + public Task Run(IProgress<double> progress, CancellationToken cancellationToken) { - get { return "Clean Database"; } - } - - public string Description - { - get { return "Deletes obsolete content from the database."; } - } - - public string Category - { - get { return "Library"; } - } - - public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) - { - // Ensure these objects are lazy loaded. - // Without this there is a deadlock that will need to be investigated - var rootChildren = _libraryManager.RootFolder.Children.ToList(); - rootChildren = _libraryManager.GetUserRootFolder().Children.ToList(); - - await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false); - - //await _itemRepo.UpdateInheritedValues(cancellationToken).ConfigureAwait(false); + return CleanDeadItems(cancellationToken, progress); } private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress) @@ -98,23 +73,5 @@ namespace Emby.Server.Implementations.Data progress.Report(100); } - - /// <summary> - /// Creates the triggers that define when the task will run - /// </summary> - /// <returns>IEnumerable{BaseTaskTrigger}.</returns> - public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() - { - return new[] { - - // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} - }; - } - - public string Key - { - get { return "CleanDatabase"; } - } } }
\ No newline at end of file diff --git a/Emby.Server.Implementations/Data/SqliteFileOrganizationRepository.cs b/Emby.Server.Implementations/Data/SqliteFileOrganizationRepository.cs deleted file mode 100644 index a254962c9..000000000 --- a/Emby.Server.Implementations/Data/SqliteFileOrganizationRepository.cs +++ /dev/null @@ -1,284 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.FileOrganization; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Querying; -using SQLitePCL.pretty; - -namespace Emby.Server.Implementations.Data -{ - public class SqliteFileOrganizationRepository : BaseSqliteRepository, IFileOrganizationRepository, IDisposable - { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - public SqliteFileOrganizationRepository(ILogger logger, IServerApplicationPaths appPaths) : base(logger) - { - DbFilePath = Path.Combine(appPaths.DataPath, "fileorganization.db"); - } - - /// <summary> - /// Opens the connection to the database - /// </summary> - /// <returns>Task.</returns> - public void Initialize() - { - using (var connection = CreateConnection()) - { - RunDefaultInitialization(connection); - - string[] queries = { - - "create table if not exists FileOrganizerResults (ResultId GUID PRIMARY KEY, OriginalPath TEXT, TargetPath TEXT, FileLength INT, OrganizationDate datetime, Status TEXT, OrganizationType TEXT, StatusMessage TEXT, ExtractedName TEXT, ExtractedYear int null, ExtractedSeasonNumber int null, ExtractedEpisodeNumber int null, ExtractedEndingEpisodeNumber, DuplicatePaths TEXT int null)", - "create index if not exists idx_FileOrganizerResults on FileOrganizerResults(ResultId)" - }; - - connection.RunQueries(queries); - } - } - - public async Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken) - { - if (result == null) - { - throw new ArgumentNullException("result"); - } - - cancellationToken.ThrowIfCancellationRequested(); - - using (WriteLock.Write()) - { - using (var connection = CreateConnection()) - { - connection.RunInTransaction(db => - { - var commandText = "replace into FileOrganizerResults (ResultId, OriginalPath, TargetPath, FileLength, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths) values (@ResultId, @OriginalPath, @TargetPath, @FileLength, @OrganizationDate, @Status, @OrganizationType, @StatusMessage, @ExtractedName, @ExtractedYear, @ExtractedSeasonNumber, @ExtractedEpisodeNumber, @ExtractedEndingEpisodeNumber, @DuplicatePaths)"; - - using (var statement = db.PrepareStatement(commandText)) - { - statement.TryBind("@ResultId", result.Id.ToGuidBlob()); - statement.TryBind("@OriginalPath", result.OriginalPath); - - statement.TryBind("@TargetPath", result.TargetPath); - statement.TryBind("@FileLength", result.FileSize); - statement.TryBind("@OrganizationDate", result.Date.ToDateTimeParamValue()); - statement.TryBind("@Status", result.Status.ToString()); - statement.TryBind("@OrganizationType", result.Type.ToString()); - statement.TryBind("@StatusMessage", result.StatusMessage); - statement.TryBind("@ExtractedName", result.ExtractedName); - statement.TryBind("@ExtractedYear", result.ExtractedYear); - statement.TryBind("@ExtractedSeasonNumber", result.ExtractedSeasonNumber); - statement.TryBind("@ExtractedEpisodeNumber", result.ExtractedEpisodeNumber); - statement.TryBind("@ExtractedEndingEpisodeNumber", result.ExtractedEndingEpisodeNumber); - statement.TryBind("@DuplicatePaths", string.Join("|", result.DuplicatePaths.ToArray())); - - statement.MoveNext(); - } - }, TransactionMode); - } - } - } - - public async Task Delete(string id) - { - if (string.IsNullOrEmpty(id)) - { - throw new ArgumentNullException("id"); - } - - using (WriteLock.Write()) - { - using (var connection = CreateConnection()) - { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("delete from FileOrganizerResults where ResultId = @ResultId")) - { - statement.TryBind("@ResultId", id.ToGuidBlob()); - statement.MoveNext(); - } - }, TransactionMode); - } - } - } - - public async Task DeleteAll() - { - using (WriteLock.Write()) - { - using (var connection = CreateConnection()) - { - connection.RunInTransaction(db => - { - var commandText = "delete from FileOrganizerResults"; - - db.Execute(commandText); - }, TransactionMode); - } - } - } - - public QueryResult<FileOrganizationResult> GetResults(FileOrganizationResultQuery query) - { - if (query == null) - { - throw new ArgumentNullException("query"); - } - - using (WriteLock.Read()) - { - using (var connection = CreateConnection(true)) - { - var commandText = "SELECT ResultId, OriginalPath, TargetPath, FileLength, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths from FileOrganizerResults"; - - if (query.StartIndex.HasValue && query.StartIndex.Value > 0) - { - commandText += string.Format(" WHERE ResultId NOT IN (SELECT ResultId FROM FileOrganizerResults ORDER BY OrganizationDate desc LIMIT {0})", - query.StartIndex.Value.ToString(_usCulture)); - } - - commandText += " ORDER BY OrganizationDate desc"; - - if (query.Limit.HasValue) - { - commandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); - } - - var list = new List<FileOrganizationResult>(); - - using (var statement = connection.PrepareStatement(commandText)) - { - foreach (var row in statement.ExecuteQuery()) - { - list.Add(GetResult(row)); - } - } - - int count; - using (var statement = connection.PrepareStatement("select count (ResultId) from FileOrganizerResults")) - { - count = statement.ExecuteQuery().SelectScalarInt().First(); - } - - return new QueryResult<FileOrganizationResult>() - { - Items = list.ToArray(), - TotalRecordCount = count - }; - } - } - } - - public FileOrganizationResult GetResult(string id) - { - if (string.IsNullOrEmpty(id)) - { - throw new ArgumentNullException("id"); - } - - using (WriteLock.Read()) - { - using (var connection = CreateConnection(true)) - { - using (var statement = connection.PrepareStatement("select ResultId, OriginalPath, TargetPath, FileLength, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths from FileOrganizerResults where ResultId=@ResultId")) - { - statement.TryBind("@ResultId", id.ToGuidBlob()); - - foreach (var row in statement.ExecuteQuery()) - { - return GetResult(row); - } - } - - return null; - } - } - } - - public FileOrganizationResult GetResult(IReadOnlyList<IResultSetValue> reader) - { - var index = 0; - - var result = new FileOrganizationResult - { - Id = reader[0].ReadGuidFromBlob().ToString("N") - }; - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - result.OriginalPath = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - result.TargetPath = reader[index].ToString(); - } - - index++; - result.FileSize = reader[index].ToInt64(); - - index++; - result.Date = reader[index].ReadDateTime(); - - index++; - result.Status = (FileSortingStatus)Enum.Parse(typeof(FileSortingStatus), reader[index].ToString(), true); - - index++; - result.Type = (FileOrganizerType)Enum.Parse(typeof(FileOrganizerType), reader[index].ToString(), true); - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - result.StatusMessage = reader[index].ToString(); - } - - result.OriginalFileName = Path.GetFileName(result.OriginalPath); - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - result.ExtractedName = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - result.ExtractedYear = reader[index].ToInt(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - result.ExtractedSeasonNumber = reader[index].ToInt(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - result.ExtractedEpisodeNumber = reader[index].ToInt(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - result.ExtractedEndingEpisodeNumber = reader[index].ToInt(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - result.DuplicatePaths = reader[index].ToString().Split('|').Where(i => !string.IsNullOrEmpty(i)).ToList(); - } - - return result; - } - } -} diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 52abbae3e..fc47809ac 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -31,6 +31,7 @@ using MediaBrowser.Model.Reflection; using SQLitePCL.pretty; using MediaBrowser.Model.System; using MediaBrowser.Model.Threading; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.Data { @@ -190,7 +191,6 @@ namespace Emby.Server.Implementations.Data AddColumn(db, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames); AddColumn(db, "TypedBaseItems", "Name", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "MediaType", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "Overview", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames); @@ -200,28 +200,22 @@ namespace Emby.Server.Implementations.Data AddColumn(db, "TypedBaseItems", "Genres", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "SortName", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "HomePageUrl", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "DisplayMediaType", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames); AddColumn(db, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames); AddColumn(db, "TypedBaseItems", "IsLive", "BIT", existingColumnNames); AddColumn(db, "TypedBaseItems", "IsNews", "BIT", existingColumnNames); AddColumn(db, "TypedBaseItems", "IsPremiere", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "IsHD", "BIT", existingColumnNames); AddColumn(db, "TypedBaseItems", "ExternalEtag", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames); - AddColumn(db, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames); AddColumn(db, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames); AddColumn(db, "TypedBaseItems", "LockedFields", "Text", existingColumnNames); @@ -233,10 +227,8 @@ namespace Emby.Server.Implementations.Data AddColumn(db, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames); AddColumn(db, "TypedBaseItems", "UnratedType", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "TopParentId", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsItemByName", "BIT", existingColumnNames); AddColumn(db, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "CriticRating", "Float", existingColumnNames); - AddColumn(db, "TypedBaseItems", "InheritedTags", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "CleanName", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames); @@ -251,7 +243,6 @@ namespace Emby.Server.Implementations.Data AddColumn(db, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames); AddColumn(db, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "Tagline", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Keywords", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "Images", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames); @@ -354,7 +345,10 @@ namespace Emby.Server.Implementations.Data // items by name "create index if not exists idx_ItemValues6 on ItemValues(ItemId,Type,CleanValue)", - "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)" + "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)", + + // Used to update inherited tags + "create index if not exists idx_ItemValues8 on ItemValues(Type, ItemId, Value)", }; connection.RunQueries(postQueries); @@ -451,10 +445,8 @@ namespace Emby.Server.Implementations.Data "SeriesId", "PresentationUniqueKey", "InheritedParentalRatingValue", - "InheritedTags", "ExternalSeriesId", "Tagline", - "Keywords", "ProviderIds", "Images", "ProductionLocations", @@ -560,10 +552,8 @@ namespace Emby.Server.Implementations.Data "IsFolder", "UnratedType", "TopParentId", - "IsItemByName", "TrailerTypes", "CriticRating", - "InheritedTags", "CleanName", "PresentationUniqueKey", "OriginalTitle", @@ -578,7 +568,6 @@ namespace Emby.Server.Implementations.Data "SeriesId", "ExternalSeriesId", "Tagline", - "Keywords", "ProviderIds", "Images", "ProductionLocations", @@ -645,7 +634,7 @@ namespace Emby.Server.Implementations.Data CheckDisposed(); - var tuples = new List<Tuple<BaseItem, List<Guid>, BaseItem, string>>(); + var tuples = new List<Tuple<BaseItem, List<Guid>, BaseItem, string, List<string>>>(); foreach (var item in items) { var ancestorIds = item.SupportsAncestors ? @@ -655,8 +644,9 @@ namespace Emby.Server.Implementations.Data var topParent = item.GetTopParent(); var userdataKey = item.GetUserDataKeys().FirstOrDefault(); + var inheritedTags = item.GetInheritedTags(); - tuples.Add(new Tuple<BaseItem, List<Guid>, BaseItem, string>(item, ancestorIds, topParent, userdataKey)); + tuples.Add(new Tuple<BaseItem, List<Guid>, BaseItem, string, List<string>>(item, ancestorIds, topParent, userdataKey, inheritedTags)); } using (WriteLock.Write()) @@ -666,12 +656,13 @@ namespace Emby.Server.Implementations.Data connection.RunInTransaction(db => { SaveItemsInTranscation(db, tuples); + }, TransactionMode); } } } - private void SaveItemsInTranscation(IDatabaseConnection db, List<Tuple<BaseItem, List<Guid>, BaseItem, string>> tuples) + private void SaveItemsInTranscation(IDatabaseConnection db, List<Tuple<BaseItem, List<Guid>, BaseItem, string, List<string>>> tuples) { var requiresReset = false; @@ -702,12 +693,14 @@ namespace Emby.Server.Implementations.Data SaveItem(item, topParent, userDataKey, saveItemStatement); //Logger.Debug(_saveItemCommand.CommandText); + var inheritedTags = tuple.Item5; + if (item.SupportsAncestors) { UpdateAncestors(item.Id, tuple.Item2, db, deleteAncestorsStatement, updateAncestorsStatement); } - UpdateItemValues(item.Id, GetItemValuesToSave(item), db); + UpdateItemValues(item.Id, GetItemValuesToSave(item, inheritedTags), db); requiresReset = true; } @@ -818,7 +811,7 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBind("@RunTimeTicks", item.RunTimeTicks); saveItemStatement.TryBind("@HomePageUrl", item.HomePageUrl); - saveItemStatement.TryBind("@DisplayMediaType", item.DisplayMediaType); + saveItemStatement.TryBindNull("@DisplayMediaType"); saveItemStatement.TryBind("@DateCreated", item.DateCreated); saveItemStatement.TryBind("@DateModified", item.DateModified); @@ -847,7 +840,7 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBind("@IsInMixedFolder", item.IsInMixedFolder); - if (item.LockedFields.Count > 0) + if (item.LockedFields.Length > 0) { saveItemStatement.TryBind("@LockedFields", string.Join("|", item.LockedFields.Select(i => i.ToString()).ToArray())); } @@ -856,7 +849,7 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBindNull("@LockedFields"); } - if (item.Studios.Count > 0) + if (item.Studios.Length > 0) { saveItemStatement.TryBind("@Studios", string.Join("|", item.Studios.ToArray())); } @@ -876,9 +869,9 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBind("@ExternalServiceId", item.ServiceName); - if (item.Tags.Count > 0) + if (item.Tags.Length > 0) { - saveItemStatement.TryBind("@Tags", string.Join("|", item.Tags.ToArray())); + saveItemStatement.TryBind("@Tags", string.Join("|", item.Tags)); } else { @@ -900,15 +893,6 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBindNull("@TopParentId"); } - var isByName = false; - var byName = item as IItemByName; - if (byName != null) - { - var dualAccess = item as IHasDualAccess; - isByName = dualAccess == null || dualAccess.IsAccessedByName; - } - saveItemStatement.TryBind("@IsItemByName", isByName); - var trailer = item as Trailer; if (trailer != null && trailer.TrailerTypes.Count > 0) { @@ -921,16 +905,6 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBind("@CriticRating", item.CriticRating); - var inheritedTags = item.InheritedTags; - if (inheritedTags.Count > 0) - { - saveItemStatement.TryBind("@InheritedTags", string.Join("|", inheritedTags.ToArray())); - } - else - { - saveItemStatement.TryBindNull("@InheritedTags"); - } - if (string.IsNullOrWhiteSpace(item.Name)) { saveItemStatement.TryBindNull("@CleanName"); @@ -1011,28 +985,19 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBind("@ExternalSeriesId", item.ExternalSeriesId); saveItemStatement.TryBind("@Tagline", item.Tagline); - if (item.Keywords.Count > 0) - { - saveItemStatement.TryBind("@Keywords", string.Join("|", item.Keywords.ToArray())); - } - else - { - saveItemStatement.TryBindNull("@Keywords"); - } - saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item)); saveItemStatement.TryBind("@Images", SerializeImages(item)); - if (item.ProductionLocations.Count > 0) + if (item.ProductionLocations.Length > 0) { - saveItemStatement.TryBind("@ProductionLocations", string.Join("|", item.ProductionLocations.ToArray())); + saveItemStatement.TryBind("@ProductionLocations", string.Join("|", item.ProductionLocations)); } else { saveItemStatement.TryBindNull("@ProductionLocations"); } - if (item.ThemeSongIds.Count > 0) + if (item.ThemeSongIds.Length > 0) { saveItemStatement.TryBind("@ThemeSongIds", string.Join("|", item.ThemeSongIds.ToArray())); } @@ -1041,7 +1006,7 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBindNull("@ThemeSongIds"); } - if (item.ThemeVideoIds.Count > 0) + if (item.ThemeVideoIds.Length > 0) { saveItemStatement.TryBind("@ThemeVideoIds", string.Join("|", item.ThemeVideoIds.ToArray())); } @@ -1075,9 +1040,9 @@ namespace Emby.Server.Implementations.Data var hasAlbumArtists = item as IHasAlbumArtist; if (hasAlbumArtists != null) { - if (hasAlbumArtists.AlbumArtists.Count > 0) + if (hasAlbumArtists.AlbumArtists.Length > 0) { - albumArtists = string.Join("|", hasAlbumArtists.AlbumArtists.ToArray()); + albumArtists = string.Join("|", hasAlbumArtists.AlbumArtists); } } saveItemStatement.TryBind("@AlbumArtists", albumArtists); @@ -1128,9 +1093,9 @@ namespace Emby.Server.Implementations.Data private string SerializeImages(BaseItem item) { - var images = item.ImageInfos.ToList(); + var images = item.ImageInfos; - if (images.Count == 0) + if (images.Length == 0) { return null; } @@ -1147,22 +1112,24 @@ namespace Emby.Server.Implementations.Data return; } - if (item.ImageInfos.Count > 0) + if (item.ImageInfos.Length > 0) { return; } var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); - + var list = new List<ItemImageInfo>(); foreach (var part in parts) { var image = ItemImageInfoFromValueString(part); if (image != null) { - item.ImageInfos.Add(image); + list.Add(image); } } + + item.ImageInfos = list.ToArray(list.Count); } public string ToValueString(ItemImageInfo image) @@ -1329,16 +1296,13 @@ namespace Emby.Server.Implementations.Data return false; } - if (_config.Configuration.SkipDeserializationForAudio) + if (type == typeof(Audio)) { - if (type == typeof(Audio)) - { - return false; - } - if (type == typeof(MusicAlbum)) - { - return false; - } + return false; + } + if (type == typeof(MusicAlbum)) + { + return false; } return true; @@ -1609,11 +1573,15 @@ namespace Emby.Server.Implementations.Data index++; } + var video = item as Video; if (HasField(query, ItemFields.DisplayMediaType)) { - if (!reader.IsDBNull(index)) + if (video != null) { - item.DisplayMediaType = reader.GetString(index); + if (!reader.IsDBNull(index)) + { + video.DisplayMediaType = reader.GetString(index); + } } index++; } @@ -1668,7 +1636,11 @@ namespace Emby.Server.Implementations.Data if (!reader.IsDBNull(index)) { - item.Audio = (ProgramAudio)Enum.Parse(typeof(ProgramAudio), reader.GetString(index), true); + ProgramAudio audio; + if (Enum.TryParse(reader.GetString(index), true, out audio)) + { + item.Audio = audio; + } } index++; @@ -1699,7 +1671,17 @@ namespace Emby.Server.Implementations.Data { if (!reader.IsDBNull(index)) { - item.LockedFields = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => (MetadataFields)Enum.Parse(typeof(MetadataFields), i, true)).ToList(); + item.LockedFields = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select( + i => + { + MetadataFields parsedValue; + + if (Enum.TryParse(i, true, out parsedValue)) + { + return parsedValue; + } + return (MetadataFields?)null; + }).Where(i => i.HasValue).Select(i => i.Value).ToArray(); } index++; } @@ -1708,7 +1690,7 @@ namespace Emby.Server.Implementations.Data { if (!reader.IsDBNull(index)) { - item.Studios = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + item.Studios = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); } index++; } @@ -1717,7 +1699,7 @@ namespace Emby.Server.Implementations.Data { if (!reader.IsDBNull(index)) { - item.Tags = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + item.Tags = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); } index++; } @@ -1729,7 +1711,18 @@ namespace Emby.Server.Implementations.Data { if (!reader.IsDBNull(index)) { - trailer.TrailerTypes = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => (TrailerType)Enum.Parse(typeof(TrailerType), i, true)).ToList(); + trailer.TrailerTypes = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select( + i => + { + TrailerType parsedValue; + + if (Enum.TryParse(i, true, out parsedValue)) + { + return parsedValue; + } + return (TrailerType?)null; + + }).Where(i => i.HasValue).Select(i => i.Value).ToList(); } } index++; @@ -1744,7 +1737,6 @@ namespace Emby.Server.Implementations.Data index++; } - var video = item as Video; if (video != null) { if (!reader.IsDBNull(index)) @@ -1847,15 +1839,6 @@ namespace Emby.Server.Implementations.Data index++; } - if (HasField(query, ItemFields.Tags)) - { - if (!reader.IsDBNull(index)) - { - item.InheritedTags = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); - } - index++; - } - if (HasField(query, ItemFields.ExternalSeriesId)) { if (!reader.IsDBNull(index)) @@ -1874,15 +1857,6 @@ namespace Emby.Server.Implementations.Data index++; } - if (HasField(query, ItemFields.Keywords)) - { - if (!reader.IsDBNull(index)) - { - item.Keywords = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); - } - index++; - } - if (!reader.IsDBNull(index)) { DeserializeProviderIds(reader.GetString(index), item); @@ -1902,7 +1876,7 @@ namespace Emby.Server.Implementations.Data { if (!reader.IsDBNull(index)) { - item.ProductionLocations = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + item.ProductionLocations = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); } index++; } @@ -1911,7 +1885,7 @@ namespace Emby.Server.Implementations.Data { if (!reader.IsDBNull(index)) { - item.ThemeSongIds = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => new Guid(i)).ToList(); + item.ThemeSongIds = SplitToGuids(reader.GetString(index)); } index++; } @@ -1920,7 +1894,7 @@ namespace Emby.Server.Implementations.Data { if (!reader.IsDBNull(index)) { - item.ThemeVideoIds = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => new Guid(i)).ToList(); + item.ThemeVideoIds = SplitToGuids(reader.GetString(index)); } index++; } @@ -1933,7 +1907,11 @@ namespace Emby.Server.Implementations.Data if (!reader.IsDBNull(index)) { - item.ExtraType = (ExtraType)Enum.Parse(typeof(ExtraType), reader.GetString(index), true); + ExtraType extraType; + if (Enum.TryParse(reader.GetString(index), true, out extraType)) + { + item.ExtraType = extraType; + } } index++; @@ -1949,7 +1927,7 @@ namespace Emby.Server.Implementations.Data var hasAlbumArtists = item as IHasAlbumArtist; if (hasAlbumArtists != null && !reader.IsDBNull(index)) { - hasAlbumArtists.AlbumArtists = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + hasAlbumArtists.AlbumArtists = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); } index++; } @@ -1975,27 +1953,28 @@ namespace Emby.Server.Implementations.Data return item; } + private Guid[] SplitToGuids(string value) + { + var ids = value.Split('|'); + + var result = new Guid[ids.Length]; + + for (var i = 0; i < result.Length; i++) + { + result[i] = new Guid(ids[i]); + } + + return result; + } + /// <summary> /// Gets the critic reviews. /// </summary> /// <param name="itemId">The item id.</param> /// <returns>Task{IEnumerable{ItemReview}}.</returns> - public IEnumerable<ItemReview> GetCriticReviews(Guid itemId) + public List<ItemReview> GetCriticReviews(Guid itemId) { - try - { - var path = Path.Combine(_criticReviewsPath, itemId + ".json"); - - return _jsonSerializer.DeserializeFromFile<List<ItemReview>>(path); - } - catch (FileNotFoundException) - { - return new List<ItemReview>(); - } - catch (IOException) - { - return new List<ItemReview>(); - } + return new List<ItemReview>(); } private readonly Task _cachedTask = Task.FromResult(true); @@ -2007,12 +1986,6 @@ namespace Emby.Server.Implementations.Data /// <returns>Task.</returns> public Task SaveCriticReviews(Guid itemId, IEnumerable<ItemReview> criticReviews) { - _fileSystem.CreateDirectory(_criticReviewsPath); - - var path = Path.Combine(_criticReviewsPath, itemId + ".json"); - - _jsonSerializer.SerializeToFile(criticReviews.ToList(), path); - return _cachedTask; } @@ -2022,7 +1995,7 @@ namespace Emby.Server.Implementations.Data /// <param name="id">The id.</param> /// <returns>IEnumerable{ChapterInfo}.</returns> /// <exception cref="System.ArgumentNullException">id</exception> - public IEnumerable<ChapterInfo> GetChapters(Guid id) + public List<ChapterInfo> GetChapters(Guid id) { CheckDisposed(); if (id == Guid.Empty) @@ -2118,18 +2091,7 @@ namespace Emby.Server.Implementations.Data /// <summary> /// Saves the chapters. /// </summary> - /// <param name="id">The id.</param> - /// <param name="chapters">The chapters.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="System.ArgumentNullException"> - /// id - /// or - /// chapters - /// or - /// cancellationToken - /// </exception> - public async Task SaveChapters(Guid id, List<ChapterInfo> chapters, CancellationToken cancellationToken) + public async Task SaveChapters(Guid id, List<ChapterInfo> chapters) { CheckDisposed(); @@ -2143,8 +2105,6 @@ namespace Emby.Server.Implementations.Data throw new ArgumentNullException("chapters"); } - cancellationToken.ThrowIfCancellationRequested(); - var index = 0; using (WriteLock.Write()) @@ -2250,7 +2210,7 @@ namespace Emby.Server.Implementations.Data return false; } - private List<ItemFields> allFields = Enum.GetNames(typeof(ItemFields)) + private readonly List<ItemFields> allFields = Enum.GetNames(typeof(ItemFields)) .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)) .ToList(); @@ -2274,7 +2234,7 @@ namespace Emby.Server.Implementations.Data } if (field == ItemFields.Tags) { - return new[] { "Tags", "InheritedTags" }; + return new[] { "Tags" }; } return new[] { field.ToString() }; @@ -2287,9 +2247,8 @@ namespace Emby.Server.Implementations.Data switch (name) { case ItemFields.HomePageUrl: - case ItemFields.Keywords: - case ItemFields.DisplayMediaType: case ItemFields.CustomRating: + case ItemFields.DisplayMediaType: case ItemFields.ProductionLocations: case ItemFields.Settings: case ItemFields.OriginalTitle: @@ -2593,11 +2552,11 @@ namespace Emby.Server.Implementations.Data } } - query.ExcludeItemIds = excludeIds.ToArray(); + query.ExcludeItemIds = excludeIds.ToArray(excludeIds.Count); query.ExcludeProviderIds = item.ProviderIds; } - return list.ToArray(); + return list.ToArray(list.Count); } private void BindSimilarParams(InternalItemsQuery query, IStatement statement) @@ -2633,9 +2592,14 @@ namespace Emby.Server.Implementations.Data groups.Add("PresentationUniqueKey"); } + if (query.GroupBySeriesPresentationUniqueKey) + { + groups.Add("SeriesPresentationUniqueKey"); + } + if (groups.Count > 0) { - return " Group by " + string.Join(",", groups.ToArray()); + return " Group by " + string.Join(",", groups.ToArray(groups.Count)); } return string.Empty; @@ -2672,7 +2636,7 @@ namespace Emby.Server.Implementations.Data var whereText = whereClauses.Count == 0 ? string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); + " where " + string.Join(" AND ", whereClauses.ToArray(whereClauses.Count)); commandText += whereText; @@ -2729,7 +2693,7 @@ namespace Emby.Server.Implementations.Data var whereText = whereClauses.Count == 0 ? string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); + " where " + string.Join(" AND ", whereClauses.ToArray(whereClauses.Count)); commandText += whereText; @@ -2849,7 +2813,7 @@ namespace Emby.Server.Implementations.Data var slowThreshold = 1000; #if DEBUG - slowThreshold = 2; + slowThreshold = 10; #endif if (elapsed >= slowThreshold) @@ -2882,7 +2846,7 @@ namespace Emby.Server.Implementations.Data var returnList = GetItemList(query); return new QueryResult<BaseItem> { - Items = returnList.ToArray(), + Items = returnList.ToArray(returnList.Count), TotalRecordCount = returnList.Count }; } @@ -2905,7 +2869,7 @@ namespace Emby.Server.Implementations.Data var whereText = whereClauses.Count == 0 ? string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); + " where " + string.Join(" AND ", whereClauses.ToArray(whereClauses.Count)); var whereTextWithoutPaging = whereText; @@ -2945,6 +2909,10 @@ namespace Emby.Server.Implementations.Data { commandText += " select count (distinct PresentationUniqueKey)" + GetFromText(); } + else if (query.GroupBySeriesPresentationUniqueKey) + { + commandText += " select count (distinct SeriesPresentationUniqueKey)" + GetFromText(); + } else { commandText += " select count (guid)" + GetFromText(); @@ -2962,8 +2930,7 @@ namespace Emby.Server.Implementations.Data return connection.RunInTransaction(db => { var result = new QueryResult<BaseItem>(); - var statements = PrepareAllSafe(db, statementTexts) - .ToList(); + var statements = PrepareAllSafe(db, statementTexts); if (!isReturningZeroItems) { @@ -3017,7 +2984,7 @@ namespace Emby.Server.Implementations.Data LogQueryTime("GetItems", commandText, now); - result.Items = list.ToArray(); + result.Items = list.ToArray(list.Count); return result; }, ReadTransactionMode); @@ -3090,6 +3057,11 @@ namespace Emby.Server.Implementations.Data } if (string.Equals(name, ItemSortBy.DatePlayed, StringComparison.OrdinalIgnoreCase)) { + if (query.GroupBySeriesPresentationUniqueKey) + { + return new Tuple<string, bool>("MAX(LastPlayedDate)", false); + } + return new Tuple<string, bool>("LastPlayedDate", false); } if (string.Equals(name, ItemSortBy.PlayCount, StringComparison.OrdinalIgnoreCase)) @@ -3164,7 +3136,7 @@ namespace Emby.Server.Implementations.Data var whereText = whereClauses.Count == 0 ? string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); + " where " + string.Join(" AND ", whereClauses.ToArray(whereClauses.Count)); commandText += whereText; @@ -3235,7 +3207,7 @@ namespace Emby.Server.Implementations.Data var whereText = whereClauses.Count == 0 ? string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); + " where " + string.Join(" AND ", whereClauses.ToArray(whereClauses.Count)); commandText += whereText; @@ -3308,7 +3280,7 @@ namespace Emby.Server.Implementations.Data var returnList = GetItemIdsList(query); return new QueryResult<Guid> { - Items = returnList.ToArray(), + Items = returnList.ToArray(returnList.Count), TotalRecordCount = returnList.Count }; } @@ -3323,7 +3295,7 @@ namespace Emby.Server.Implementations.Data var whereText = whereClauses.Count == 0 ? string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); + " where " + string.Join(" AND ", whereClauses.ToArray(whereClauses.Count)); var whereTextWithoutPaging = whereText; @@ -3364,6 +3336,10 @@ namespace Emby.Server.Implementations.Data { commandText += " select count (distinct PresentationUniqueKey)" + GetFromText(); } + else if (query.GroupBySeriesPresentationUniqueKey) + { + commandText += " select count (distinct SeriesPresentationUniqueKey)" + GetFromText(); + } else { commandText += " select count (guid)" + GetFromText(); @@ -3382,8 +3358,7 @@ namespace Emby.Server.Implementations.Data { var result = new QueryResult<Guid>(); - var statements = PrepareAllSafe(db, statementTexts) - .ToList(); + var statements = PrepareAllSafe(db, statementTexts); if (!isReturningZeroItems) { @@ -3426,7 +3401,7 @@ namespace Emby.Server.Implementations.Data LogQueryTime("GetItemIds", commandText, now); - result.Items = list.ToArray(); + result.Items = list.ToArray(list.Count); return result; }, ReadTransactionMode); @@ -3631,7 +3606,7 @@ namespace Emby.Server.Implementations.Data } if (programAttribtues.Count > 0) { - whereClauses.Add("(" + string.Join(" OR ", programAttribtues.ToArray()) + ")"); + whereClauses.Add("(" + string.Join(" OR ", programAttribtues.ToArray(programAttribtues.Count)) + ")"); } } @@ -4221,23 +4196,6 @@ namespace Emby.Server.Implementations.Data whereClauses.Add(clause); } - if (query.Keywords.Length > 0) - { - var clauses = new List<string>(); - var index = 0; - foreach (var item in query.Keywords) - { - clauses.Add("@Keyword" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=5)"); - if (statement != null) - { - statement.TryBind("@Keyword" + index, GetCleanValue(item)); - } - index++; - } - var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; - whereClauses.Add(clause); - } - if (query.OfficialRatings.Length > 0) { var clauses = new List<string>(); @@ -4317,12 +4275,13 @@ namespace Emby.Server.Implementations.Data whereClauses.Add("ProductionYear in (" + val + ")"); } - if (query.IsVirtualItem.HasValue) + var isVirtualItem = query.IsVirtualItem ?? query.IsMissing; + if (isVirtualItem.HasValue) { whereClauses.Add("IsVirtualItem=@IsVirtualItem"); if (statement != null) { - statement.TryBind("@IsVirtualItem", query.IsVirtualItem.Value); + statement.TryBind("@IsVirtualItem", isVirtualItem.Value); } } if (query.IsSpecialSeason.HasValue) @@ -4347,28 +4306,6 @@ namespace Emby.Server.Implementations.Data whereClauses.Add("PremiereDate < DATETIME('now')"); } } - if (query.IsMissing.HasValue) - { - if (query.IsMissing.Value) - { - whereClauses.Add("(IsVirtualItem=1 AND PremiereDate < DATETIME('now'))"); - } - else - { - whereClauses.Add("(IsVirtualItem=0 OR PremiereDate >= DATETIME('now'))"); - } - } - if (query.IsVirtualUnaired.HasValue) - { - if (query.IsVirtualUnaired.Value) - { - whereClauses.Add("(IsVirtualItem=1 AND PremiereDate >= DATETIME('now'))"); - } - else - { - whereClauses.Add("(IsVirtualItem=0 OR PremiereDate < DATETIME('now'))"); - } - } var queryMediaTypes = query.MediaTypes.Where(IsValidMediaType).ToArray(); if (queryMediaTypes.Length == 1) { @@ -4483,21 +4420,27 @@ namespace Emby.Server.Implementations.Data } } - //var enableItemsByName = query.IncludeItemsByName ?? query.IncludeItemTypes.Length > 0; - var enableItemsByName = query.IncludeItemsByName ?? false; + + var includedItemByNameTypes = GetItemByNameTypesInQuery(query).SelectMany(MapIncludeItemTypes).ToList(); + var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0; var queryTopParentIds = query.TopParentIds.Where(IsValidId).ToArray(); if (queryTopParentIds.Length == 1) { - if (enableItemsByName) + if (enableItemsByName && includedItemByNameTypes.Count == 1) { - whereClauses.Add("(TopParentId=@TopParentId or IsItemByName=@IsItemByName)"); + whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)"); if (statement != null) { - statement.TryBind("@IsItemByName", true); + statement.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]); } } + else if (enableItemsByName && includedItemByNameTypes.Count > 1) + { + var itemByNameTypeVal = string.Join(",", includedItemByNameTypes.Select(i => "'" + i + "'").ToArray()); + whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))"); + } else { whereClauses.Add("(TopParentId=@TopParentId)"); @@ -4511,14 +4454,19 @@ namespace Emby.Server.Implementations.Data { var val = string.Join(",", queryTopParentIds.Select(i => "'" + i + "'").ToArray()); - if (enableItemsByName) + if (enableItemsByName && includedItemByNameTypes.Count == 1) { - whereClauses.Add("(IsItemByName=@IsItemByName or TopParentId in (" + val + "))"); + whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))"); if (statement != null) { - statement.TryBind("@IsItemByName", true); + statement.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]); } } + else if (enableItemsByName && includedItemByNameTypes.Count > 1) + { + var itemByNameTypeVal = string.Join(",", includedItemByNameTypes.Select(i => "'" + i + "'").ToArray()); + whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))"); + } else { whereClauses.Add("(TopParentId in (" + val + "))"); @@ -4573,29 +4521,57 @@ namespace Emby.Server.Implementations.Data whereClauses.Add(string.Format("(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))", inClause)); } - var excludeTagIndex = 0; - foreach (var excludeTag in query.ExcludeTags) + if (query.ExcludeInheritedTags.Length > 0) { - whereClauses.Add("(Tags is null OR Tags not like @excludeTag" + excludeTagIndex + ")"); - if (statement != null) - { - statement.TryBind("@excludeTag" + excludeTagIndex, "%" + excludeTag + "%"); - } - excludeTagIndex++; + var tagValues = query.ExcludeInheritedTags.Select(i => "'" + GetCleanValue(i) + "'").ToArray(); + var tagValuesList = string.Join(",", tagValues); + + whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + tagValuesList + ")) is null)"); } - excludeTagIndex = 0; - foreach (var excludeTag in query.ExcludeInheritedTags) + return whereClauses; + } + + private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query) + { + var list = new List<string>(); + + if (IsTypeInQuery(typeof(Person).Name, query)) { - whereClauses.Add("(InheritedTags is null OR InheritedTags not like @excludeInheritedTag" + excludeTagIndex + ")"); - if (statement != null) - { - statement.TryBind("@excludeInheritedTag" + excludeTagIndex, "%" + excludeTag + "%"); - } - excludeTagIndex++; + list.Add(typeof(Person).Name); + } + if (IsTypeInQuery(typeof(Genre).Name, query)) + { + list.Add(typeof(Genre).Name); + } + if (IsTypeInQuery(typeof(MusicGenre).Name, query)) + { + list.Add(typeof(MusicGenre).Name); + } + if (IsTypeInQuery(typeof(GameGenre).Name, query)) + { + list.Add(typeof(GameGenre).Name); + } + if (IsTypeInQuery(typeof(MusicArtist).Name, query)) + { + list.Add(typeof(MusicArtist).Name); + } + if (IsTypeInQuery(typeof(Studio).Name, query)) + { + list.Add(typeof(Studio).Name); } - return whereClauses; + return list; + } + + private bool IsTypeInQuery(string type, InternalItemsQuery query) + { + if (query.ExcludeItemTypes.Contains(type, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type, StringComparer.OrdinalIgnoreCase); } private string GetCleanValue(string value) @@ -4615,6 +4591,11 @@ namespace Emby.Server.Implementations.Data return false; } + if (query.GroupBySeriesPresentationUniqueKey) + { + return false; + } + if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) { return false; @@ -4693,41 +4674,65 @@ namespace Emby.Server.Implementations.Data private async Task UpdateInheritedTags(CancellationToken cancellationToken) { - var newValues = new List<Tuple<Guid, string>>(); + var newValues = new List<Tuple<Guid, string[]>>(); - var commandText = "select Guid,InheritedTags,(select group_concat(Tags, '|') from TypedBaseItems where (guid=outer.guid) OR (guid in (Select AncestorId from AncestorIds where ItemId=Outer.guid))) as NewInheritedTags from typedbaseitems as Outer where NewInheritedTags <> InheritedTags"; + var commandText = @"select guid, +(select group_concat(Value, '|') from ItemValues where (ItemValues.ItemId = Outer.Guid OR ItemValues.ItemId in ((Select AncestorId from AncestorIds where AncestorIds.ItemId=Outer.guid))) and ItemValues.Type = 4) NewInheritedTags, +(select group_concat(Value, '|') from ItemValues where ItemValues.ItemId = Outer.Guid and ItemValues.Type = 6) CurrentInheritedTags +from typedbaseitems as Outer +where (NewInheritedTags <> CurrentInheritedTags or (NewInheritedTags is null) <> (CurrentInheritedTags is null)) +limit 100"; using (WriteLock.Write()) { using (var connection = CreateConnection()) { - foreach (var row in connection.Query(commandText)) + connection.RunInTransaction(db => { - var id = row.GetGuid(0); - string value = row.IsDBNull(2) ? null : row.GetString(2); + foreach (var row in connection.Query(commandText)) + { + var id = row.GetGuid(0); + string value = row.IsDBNull(1) ? null : row.GetString(1); - newValues.Add(new Tuple<Guid, string>(id, value)); - } + var valuesArray = string.IsNullOrWhiteSpace(value) ? new string[] { } : value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); - Logger.Debug("UpdateInheritedTags - {0} rows", newValues.Count); - if (newValues.Count == 0) - { - return; - } + newValues.Add(new Tuple<Guid, string[]>(id, valuesArray)); + } - // write lock here - using (var statement = PrepareStatement(connection, "Update TypedBaseItems set InheritedTags=@InheritedTags where Guid=@Guid")) - { - foreach (var item in newValues) + Logger.Debug("UpdateInheritedTags - {0} rows", newValues.Count); + if (newValues.Count == 0) { - var paramList = new List<object>(); + return; + } + + using (var insertStatement = PrepareStatement(connection, "insert into ItemValues (ItemId, Type, Value, CleanValue) values (@ItemId, 6, @Value, @CleanValue)")) + { + using (var deleteStatement = PrepareStatement(connection, "delete from ItemValues where ItemId=@ItemId and Type=6")) + { + foreach (var item in newValues) + { + var guidBlob = item.Item1.ToGuidBlob(); - paramList.Add(item.Item1); - paramList.Add(item.Item2); + deleteStatement.Reset(); + deleteStatement.TryBind("@ItemId", guidBlob); + deleteStatement.MoveNext(); - statement.Execute(paramList.ToArray()); + foreach (var itemValue in item.Item2) + { + insertStatement.Reset(); + + insertStatement.TryBind("@ItemId", guidBlob); + insertStatement.TryBind("@Value", itemValue); + + insertStatement.TryBind("@CleanValue", GetCleanValue(itemValue)); + + insertStatement.MoveNext(); + } + } + } } - } + + }, TransactionMode); } } } @@ -5129,9 +5134,9 @@ namespace Emby.Server.Implementations.Data var itemCountColumns = new List<Tuple<string, string>>(); - var typesToCount = query.IncludeItemTypes.ToList(); + var typesToCount = query.IncludeItemTypes; - if (typesToCount.Count > 0) + if (typesToCount.Length > 0) { var itemCountColumnQuery = "select group_concat(type, '|')" + GetFromText("B"); @@ -5191,7 +5196,7 @@ namespace Emby.Server.Implementations.Data var whereText = " where Type=@SelectType"; - if (typesToCount.Count == 0) + if (typesToCount.Length == 0) { whereText += " And CleanName In (Select CleanValue from ItemValues where " + typeClause + " AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))"; } @@ -5269,8 +5274,7 @@ namespace Emby.Server.Implementations.Data var list = new List<Tuple<BaseItem, ItemCounts>>(); var result = new QueryResult<Tuple<BaseItem, ItemCounts>>(); - var statements = PrepareAllSafe(db, statementTexts) - .ToList(); + var statements = PrepareAllSafe(db, statementTexts); if (!isReturningZeroItems) { @@ -5345,7 +5349,7 @@ namespace Emby.Server.Implementations.Data { result.TotalRecordCount = list.Count; } - result.Items = list.ToArray(); + result.Items = list.ToArray(list.Count); return result; @@ -5354,11 +5358,11 @@ namespace Emby.Server.Implementations.Data } } - private ItemCounts GetItemCounts(IReadOnlyList<IResultSetValue> reader, int countStartColumn, List<string> typesToCount) + private ItemCounts GetItemCounts(IReadOnlyList<IResultSetValue> reader, int countStartColumn, string[] typesToCount) { var counts = new ItemCounts(); - if (typesToCount.Count == 0) + if (typesToCount.Length == 0) { return counts; } @@ -5416,7 +5420,7 @@ namespace Emby.Server.Implementations.Data return counts; } - private List<Tuple<int, string>> GetItemValuesToSave(BaseItem item) + private List<Tuple<int, string>> GetItemValuesToSave(BaseItem item, List<string> inheritedTags) { var list = new List<Tuple<int, string>>(); @@ -5435,7 +5439,10 @@ namespace Emby.Server.Implementations.Data list.AddRange(item.Genres.Select(i => new Tuple<int, string>(2, i))); list.AddRange(item.Studios.Select(i => new Tuple<int, string>(3, i))); list.AddRange(item.Tags.Select(i => new Tuple<int, string>(4, i))); - list.AddRange(item.Keywords.Select(i => new Tuple<int, string>(5, i))); + + // keywords was 5 + + list.AddRange(inheritedTags.Select(i => new Tuple<int, string>(6, i))); return list; } @@ -5454,8 +5461,10 @@ namespace Emby.Server.Implementations.Data CheckDisposed(); + var guidBlob = itemId.ToGuidBlob(); + // First delete - db.Execute("delete from ItemValues where ItemId=@Id", itemId.ToGuidBlob()); + db.Execute("delete from ItemValues where ItemId=@Id", guidBlob); using (var statement = PrepareStatement(db, "insert into ItemValues (ItemId, Type, Value, CleanValue) values (@ItemId, @Type, @Value, @CleanValue)")) { @@ -5471,7 +5480,7 @@ namespace Emby.Server.Implementations.Data statement.Reset(); - statement.TryBind("@ItemId", itemId.ToGuidBlob()); + statement.TryBind("@ItemId", guidBlob); statement.TryBind("@Type", pair.Item1); statement.TryBind("@Value", itemValue); @@ -5563,7 +5572,7 @@ namespace Emby.Server.Implementations.Data return item; } - public IEnumerable<MediaStream> GetMediaStreams(MediaStreamQuery query) + public List<MediaStream> GetMediaStreams(MediaStreamQuery query) { CheckDisposed(); diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 2933a9f22..e1d0a1858 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -359,11 +359,11 @@ namespace Emby.Server.Implementations.Dto { if (user == null) { - dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(hasMediaSources, true).ToList(); + dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(hasMediaSources, true); } else { - dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(hasMediaSources, true, user).ToList(); + dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(hasMediaSources, true, user); } } } @@ -517,7 +517,7 @@ namespace Emby.Server.Implementations.Dto } } - if (!(item is LiveTvProgram) || fields.Contains(ItemFields.PlayAccess)) + if (/*!(item is LiveTvProgram) ||*/ fields.Contains(ItemFields.PlayAccess)) { dto.PlayAccess = item.GetPlayAccess(user); } @@ -639,7 +639,6 @@ namespace Emby.Server.Implementations.Dto private void SetGameProperties(BaseItemDto dto, Game item) { - dto.Players = item.PlayersSupported; dto.GameSystem = item.GameSystem; dto.MultiPartGameFiles = item.MultiPartGameFiles; } @@ -649,12 +648,12 @@ namespace Emby.Server.Implementations.Dto dto.GameSystem = item.GameSystemName; } - private List<string> GetImageTags(BaseItem item, List<ItemImageInfo> images) + private string[] GetImageTags(BaseItem item, List<ItemImageInfo> images) { return images .Select(p => GetImageCacheTag(item, p)) .Where(i => i != null) - .ToList(); + .ToArray(); } private string GetImageCacheTag(BaseItem item, ImageType type) @@ -766,7 +765,7 @@ namespace Emby.Server.Implementations.Dto } } - dto.People = list.ToArray(); + dto.People = list.ToArray(list.Count); } /// <summary> @@ -864,11 +863,6 @@ namespace Emby.Server.Implementations.Dto dto.DateCreated = item.DateCreated; } - if (fields.Contains(ItemFields.DisplayMediaType)) - { - dto.DisplayMediaType = item.DisplayMediaType; - } - if (fields.Contains(ItemFields.Settings)) { dto.LockedFields = item.LockedFields; @@ -894,11 +888,6 @@ namespace Emby.Server.Implementations.Dto dto.Tags = item.Tags; } - if (fields.Contains(ItemFields.Keywords)) - { - dto.Keywords = item.Keywords; - } - var hasAspectRatio = item as IHasAspectRatio; if (hasAspectRatio != null) { @@ -1003,7 +992,7 @@ namespace Emby.Server.Implementations.Dto { dto.RemoteTrailers = hasTrailers != null ? hasTrailers.RemoteTrailers : - new List<MediaUrl>(); + new MediaUrl[] {}; } dto.Name = item.Name; @@ -1059,12 +1048,12 @@ namespace Emby.Server.Implementations.Dto { if (!string.IsNullOrWhiteSpace(item.Tagline)) { - dto.Taglines = new List<string> { item.Tagline }; + dto.Taglines = new string[] { item.Tagline }; } if (dto.Taglines == null) { - dto.Taglines = new List<string>(); + dto.Taglines = new string[]{}; } } @@ -1130,8 +1119,7 @@ namespace Emby.Server.Implementations.Dto // Include artists that are not in the database yet, e.g., just added via metadata editor //var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList(); - dto.ArtistItems = new List<NameIdPair>(); - dto.ArtistItems.AddRange(hasArtist.Artists + dto.ArtistItems = hasArtist.Artists //.Except(foundArtists, new DistinctNameComparer()) .Select(i => { @@ -1156,7 +1144,7 @@ namespace Emby.Server.Implementations.Dto return null; - }).Where(i => i != null)); + }).Where(i => i != null).ToArray(); } var hasAlbumArtist = item as IHasAlbumArtist; @@ -1182,8 +1170,7 @@ namespace Emby.Server.Implementations.Dto // }) // .ToList(); - dto.AlbumArtists = new List<NameIdPair>(); - dto.AlbumArtists.AddRange(hasAlbumArtist.AlbumArtists + dto.AlbumArtists = hasAlbumArtist.AlbumArtists //.Except(foundArtists, new DistinctNameComparer()) .Select(i => { @@ -1208,7 +1195,7 @@ namespace Emby.Server.Implementations.Dto return null; - }).Where(i => i != null)); + }).Where(i => i != null).ToArray(); } // Add video info @@ -1224,9 +1211,9 @@ namespace Emby.Server.Implementations.Dto dto.HasSubtitles = video.HasSubtitles; } - if (video.AdditionalParts.Count != 0) + if (video.AdditionalParts.Length != 0) { - dto.PartCount = video.AdditionalParts.Count + 1; + dto.PartCount = video.AdditionalParts.Length + 1; } if (fields.Contains(ItemFields.MediaSourceCount)) @@ -1276,7 +1263,7 @@ namespace Emby.Server.Implementations.Dto var hasSpecialFeatures = item as IHasSpecialFeatures; if (hasSpecialFeatures != null) { - var specialFeatureCount = hasSpecialFeatures.SpecialFeatureIds.Count; + var specialFeatureCount = hasSpecialFeatures.SpecialFeatureIds.Length; if (specialFeatureCount > 0) { @@ -1321,15 +1308,6 @@ namespace Emby.Server.Implementations.Dto Series episodeSeries = null; - if (fields.Contains(ItemFields.SeriesGenres)) - { - episodeSeries = episodeSeries ?? episode.Series; - if (episodeSeries != null) - { - dto.SeriesGenres = episodeSeries.Genres.ToList(); - } - } - //if (fields.Contains(ItemFields.SeriesPrimaryImage)) { episodeSeries = episodeSeries ?? episode.Series; @@ -1345,27 +1323,6 @@ namespace Emby.Server.Implementations.Dto if (episodeSeries != null) { dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault(); - if (!string.IsNullOrWhiteSpace(dto.SeriesStudio)) - { - try - { - var studio = _libraryManager.GetStudio(dto.SeriesStudio); - - if (studio != null) - { - dto.SeriesStudioInfo = new StudioDto - { - Name = dto.SeriesStudio, - Id = studio.Id.ToString("N"), - PrimaryImageTag = GetImageCacheTag(studio, ImageType.Primary) - }; - } - } - catch (Exception ex) - { - - } - } } } } @@ -1440,9 +1397,9 @@ namespace Emby.Server.Implementations.Dto if (fields.Contains(ItemFields.ProductionLocations)) { - if (item.ProductionLocations.Count > 0 || item is Movie) + if (item.ProductionLocations.Length > 0 || item is Movie) { - dto.ProductionLocations = item.ProductionLocations.ToArray(); + dto.ProductionLocations = item.ProductionLocations; } } @@ -1509,7 +1466,9 @@ namespace Emby.Server.Implementations.Dto BaseItem parent = null; var isFirst = true; - while (((!dto.HasLogo && logoLimit > 0) || (!dto.HasArtImage && artLimit > 0) || (!dto.HasThumb && thumbLimit > 0) || parent is Series) && + var imageTags = dto.ImageTags; + + while (((!(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0) || parent is Series) && (parent = parent ?? (isFirst ? GetImageDisplayParent(item, item) ?? owner : parent)) != null) { if (parent == null) @@ -1519,7 +1478,7 @@ namespace Emby.Server.Implementations.Dto var allImages = parent.ImageInfos; - if (logoLimit > 0 && !dto.HasLogo && dto.ParentLogoItemId == null) + if (logoLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && dto.ParentLogoItemId == null) { var image = allImages.FirstOrDefault(i => i.Type == ImageType.Logo); @@ -1529,7 +1488,7 @@ namespace Emby.Server.Implementations.Dto dto.ParentLogoImageTag = GetImageCacheTag(parent, image); } } - if (artLimit > 0 && !dto.HasArtImage && dto.ParentArtItemId == null) + if (artLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtItemId == null) { var image = allImages.FirstOrDefault(i => i.Type == ImageType.Art); @@ -1539,7 +1498,7 @@ namespace Emby.Server.Implementations.Dto dto.ParentArtImageTag = GetImageCacheTag(parent, image); } } - if (thumbLimit > 0 && !dto.HasThumb && (dto.ParentThumbItemId == null || parent is Series) && !(parent is ICollectionFolder) && !(parent is UserView)) + if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && !(parent is ICollectionFolder) && !(parent is UserView)) { var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb); @@ -1549,7 +1508,7 @@ namespace Emby.Server.Implementations.Dto dto.ParentThumbImageTag = GetImageCacheTag(parent, image); } } - if (backdropLimit > 0 && !dto.HasBackdrop) + if (backdropLimit > 0 && !((dto.BackdropImageTags != null && dto.BackdropImageTags.Length > 0) || (dto.ParentBackdropImageTags != null && dto.ParentBackdropImageTags.Length > 0))) { var images = allImages.Where(i => i.Type == ImageType.Backdrop).Take(backdropLimit).ToList(); @@ -1591,12 +1550,12 @@ namespace Emby.Server.Implementations.Dto /// <param name="dto">The dto.</param> /// <param name="item">The item.</param> /// <returns>Task.</returns> - public void AttachPrimaryImageAspectRatio(IItemDto dto, IHasImages item) + public void AttachPrimaryImageAspectRatio(IItemDto dto, IHasMetadata item) { dto.PrimaryImageAspectRatio = GetPrimaryImageAspectRatio(item); } - public double? GetPrimaryImageAspectRatio(IHasImages item) + public double? GetPrimaryImageAspectRatio(IHasMetadata item) { var imageInfo = item.GetImageInfo(ImageType.Primary, 0); diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 1b0cbb936..38f51919a 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -21,6 +21,7 @@ <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> @@ -29,6 +30,7 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> </PropertyGroup> <ItemGroup> <Compile Include="..\SharedVersion.cs"> @@ -40,6 +42,8 @@ <Compile Include="AppBase\BaseApplicationPaths.cs" /> <Compile Include="AppBase\BaseConfigurationManager.cs" /> <Compile Include="AppBase\ConfigurationHelper.cs" /> + <Compile Include="ApplicationHost.cs" /> + <Compile Include="ApplicationPathHelper.cs" /> <Compile Include="Branding\BrandingConfigurationFactory.cs" /> <Compile Include="Browser\BrowserLauncher.cs" /> <Compile Include="Channels\ChannelConfigurations.cs" /> @@ -52,9 +56,26 @@ <Compile Include="Collections\CollectionManager.cs" /> <Compile Include="Collections\CollectionsDynamicFolder.cs" /> <Compile Include="Configuration\ServerConfigurationManager.cs" /> + <Compile Include="Cryptography\ASN1.cs" /> + <Compile Include="Cryptography\ASN1Convert.cs" /> + <Compile Include="Cryptography\BitConverterLE.cs" /> + <Compile Include="Cryptography\CertificateGenerator.cs" /> + <Compile Include="Cryptography\CryptoConvert.cs" /> + <Compile Include="Cryptography\PfxGenerator.cs" /> + <Compile Include="Cryptography\PKCS1.cs" /> + <Compile Include="Cryptography\PKCS12.cs" /> + <Compile Include="Cryptography\PKCS7.cs" /> + <Compile Include="Cryptography\PKCS8.cs" /> + <Compile Include="Cryptography\X501Name.cs" /> + <Compile Include="Cryptography\X509Builder.cs" /> + <Compile Include="Cryptography\X509Certificate.cs" /> + <Compile Include="Cryptography\X509CertificateBuilder.cs" /> + <Compile Include="Cryptography\X509CertificateCollection.cs" /> + <Compile Include="Cryptography\X509Extension.cs" /> + <Compile Include="Cryptography\X509Extensions.cs" /> + <Compile Include="Cryptography\X520Attributes.cs" /> <Compile Include="Data\ManagedConnection.cs" /> <Compile Include="Data\SqliteDisplayPreferencesRepository.cs" /> - <Compile Include="Data\SqliteFileOrganizationRepository.cs" /> <Compile Include="Data\SqliteItemRepository.cs" /> <Compile Include="Data\SqliteUserDataRepository.cs" /> <Compile Include="Data\SqliteUserRepository.cs" /> @@ -64,6 +85,7 @@ <Compile Include="Devices\DeviceRepository.cs" /> <Compile Include="Dto\DtoService.cs" /> <Compile Include="EntryPoints\AutomaticRestartEntryPoint.cs" /> + <Compile Include="EntryPoints\ExternalPortForwarding.cs" /> <Compile Include="EntryPoints\KeepServerAwake.cs" /> <Compile Include="EntryPoints\LibraryChangedNotifier.cs" /> <Compile Include="EntryPoints\LoadRegistrations.cs" /> @@ -79,13 +101,7 @@ <Compile Include="FFMpeg\FFMpegInfo.cs" /> <Compile Include="FFMpeg\FFMpegInstallInfo.cs" /> <Compile Include="FFMpeg\FFMpegLoader.cs" /> - <Compile Include="FileOrganization\EpisodeFileOrganizer.cs" /> - <Compile Include="FileOrganization\Extensions.cs" /> - <Compile Include="FileOrganization\FileOrganizationNotifier.cs" /> - <Compile Include="FileOrganization\FileOrganizationService.cs" /> - <Compile Include="FileOrganization\NameUtils.cs" /> - <Compile Include="FileOrganization\OrganizerScheduledTask.cs" /> - <Compile Include="FileOrganization\TvFolderOrganizer.cs" /> + <Compile Include="HttpServerFactory.cs" /> <Compile Include="HttpServer\FileWriter.cs" /> <Compile Include="HttpServer\HttpListenerHost.cs" /> <Compile Include="HttpServer\HttpResultFactory.cs" /> @@ -107,7 +123,9 @@ <Compile Include="Images\BaseDynamicImageProvider.cs" /> <Compile Include="IO\AsyncStreamCopier.cs" /> <Compile Include="IO\FileRefresher.cs" /> + <Compile Include="IO\LibraryMonitor.cs" /> <Compile Include="IO\MbLinkShortcutHandler.cs" /> + <Compile Include="IO\MemoryStreamProvider.cs" /> <Compile Include="IO\ThrottledStream.cs" /> <Compile Include="Library\CoreResolutionIgnoreRule.cs" /> <Compile Include="Library\LibraryManager.cs" /> @@ -178,6 +196,8 @@ <Compile Include="LiveTv\TunerHosts\MulticastStream.cs" /> <Compile Include="LiveTv\TunerHosts\QueueStream.cs" /> <Compile Include="Localization\LocalizationManager.cs" /> + <Compile Include="Localization\TextLocalizer.cs" /> + <Compile Include="Logging\ConsoleLogger.cs" /> <Compile Include="Logging\UnhandledExceptionWriter.cs" /> <Compile Include="MediaEncoder\EncodingManager.cs" /> <Compile Include="Migrations\IVersionMigration.cs" /> @@ -229,7 +249,6 @@ <Compile Include="Social\SharingManager.cs" /> <Compile Include="Social\SharingRepository.cs" /> <Compile Include="Sorting\AiredEpisodeOrderComparer.cs" /> - <Compile Include="Sorting\AirTimeComparer.cs" /> <Compile Include="Sorting\AlbumArtistComparer.cs" /> <Compile Include="Sorting\AlbumComparer.cs" /> <Compile Include="Sorting\AlphanumComparator.cs" /> @@ -257,6 +276,7 @@ <Compile Include="Sorting\StartDateComparer.cs" /> <Compile Include="Sorting\StudioComparer.cs" /> <Compile Include="StartupOptions.cs" /> + <Compile Include="SystemEvents.cs" /> <Compile Include="TV\SeriesPostScanTask.cs" /> <Compile Include="TV\TVSeriesManager.cs" /> <Compile Include="Udp\UdpServer.cs" /> @@ -268,6 +288,26 @@ <EmbeddedResource Include="Localization\iso6392.txt" /> </ItemGroup> <ItemGroup> + <ProjectReference Include="..\Emby.Common.Implementations\Emby.Common.Implementations.csproj"> + <Project>{1e37a338-9f57-4b70-bd6d-bb9c591e319b}</Project> + <Name>Emby.Common.Implementations</Name> + </ProjectReference> + <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj"> + <Project>{805844ab-e92f-45e6-9d99-4f6d48d129a5}</Project> + <Name>Emby.Dlna</Name> + </ProjectReference> + <ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj"> + <Project>{08fff49b-f175-4807-a2b5-73b0ebd9f716}</Project> + <Name>Emby.Drawing</Name> + </ProjectReference> + <ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj"> + <Project>{89ab4548-770d-41fd-a891-8daff44f452c}</Project> + <Name>Emby.Photos</Name> + </ProjectReference> + <ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj"> + <Project>{4fd51ac5-2c16-4308-a993-c3a84f3b4582}</Project> + <Name>MediaBrowser.Api</Name> + </ProjectReference> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj"> <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project> <Name>MediaBrowser.Common</Name> @@ -276,6 +316,10 @@ <Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project> <Name>MediaBrowser.Controller</Name> </ProjectReference> + <ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj"> + <Project>{7ef9f3e0-697d-42f3-a08f-19deb5f84392}</Project> + <Name>MediaBrowser.LocalMetadata</Name> + </ProjectReference> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj"> <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project> <Name>MediaBrowser.Model</Name> @@ -288,10 +332,29 @@ <Project>{2e781478-814d-4a48-9d80-bff206441a65}</Project> <Name>MediaBrowser.Server.Implementations</Name> </ProjectReference> + <ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj"> + <Project>{5624b7b5-b5a7-41d8-9f10-cc5611109619}</Project> + <Name>MediaBrowser.WebDashboard</Name> + </ProjectReference> + <ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj"> + <Project>{23499896-b135-4527-8574-c26e926ea99e}</Project> + <Name>MediaBrowser.XbmcMetadata</Name> + </ProjectReference> + <ProjectReference Include="..\Mono.Nat\Mono.Nat.csproj"> + <Project>{cb7f2326-6497-4a3d-ba03-48513b17a7be}</Project> + <Name>Mono.Nat</Name> + </ProjectReference> + <ProjectReference Include="..\OpenSubtitlesHandler\OpenSubtitlesHandler.csproj"> + <Project>{4a4402d4-e910-443b-b8fc-2c18286a2ca0}</Project> + <Name>OpenSubtitlesHandler</Name> + </ProjectReference> <ProjectReference Include="..\SocketHttpListener\SocketHttpListener.csproj"> <Project>{1d74413b-e7cf-455b-b021-f52bdf881542}</Project> <Name>SocketHttpListener</Name> </ProjectReference> + <Reference Include="Emby.Server.MediaEncoding"> + <HintPath>..\ThirdParty\emby\Emby.Server.MediaEncoding.dll</HintPath> + </Reference> <Reference Include="Emby.XmlTv, Version=1.0.6387.29335, Culture=neutral, processorArchitecture=MSIL"> <HintPath>..\packages\Emby.XmlTv.1.0.9\lib\portable-net45+win8\Emby.XmlTv.dll</HintPath> <Private>True</Private> @@ -300,6 +363,16 @@ <HintPath>..\packages\MediaBrowser.Naming.1.0.5\lib\portable-net45+win8\MediaBrowser.Naming.dll</HintPath> <Private>True</Private> </Reference> + <Reference Include="Microsoft.IO.RecyclableMemoryStream, Version=1.2.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.IO.RecyclableMemoryStream.1.2.2\lib\net45\Microsoft.IO.RecyclableMemoryStream.dll</HintPath> + </Reference> + <Reference Include="ServiceStack.Text, Version=4.5.8.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\ServiceStack.Text.4.5.8\lib\net45\ServiceStack.Text.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="SimpleInjector, Version=4.0.8.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL"> + <HintPath>..\packages\SimpleInjector.4.0.8\lib\net45\SimpleInjector.dll</HintPath> + </Reference> <Reference Include="SQLitePCL.pretty, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> <HintPath>..\packages\SQLitePCL.pretty.1.1.0\lib\portable-net45+netcore45+wpa81+wp8\SQLitePCL.pretty.dll</HintPath> <Private>True</Private> @@ -307,10 +380,10 @@ </ItemGroup> <ItemGroup> <Reference Include="SQLitePCLRaw.core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1488e028ca7ab535, processorArchitecture=MSIL"> - <HintPath>..\packages\SQLitePCLRaw.core.1.1.7\lib\net45\SQLitePCLRaw.core.dll</HintPath> - <Private>True</Private> + <HintPath>..\packages\SQLitePCLRaw.core.1.1.8\lib\net45\SQLitePCLRaw.core.dll</HintPath> </Reference> <Reference Include="System" /> + <Reference Include="System.Configuration" /> <Reference Include="System.Core" /> <Reference Include="System.Runtime.Serialization" /> <Reference Include="System.Xml.Linq" /> @@ -421,6 +494,9 @@ <ItemGroup> <EmbeddedResource Include="Localization\Ratings\es.txt" /> </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Localization\Ratings\ro.txt" /> + </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <!-- To modify your build process, add your task inside one of the targets below and uncomment it. Other similar extension points exist, see Microsoft.Common.targets. diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs new file mode 100644 index 000000000..c96799b2f --- /dev/null +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Threading; +using Mono.Nat; +using MediaBrowser.Model.Extensions; + +namespace Emby.Server.Implementations.EntryPoints +{ + public class ExternalPortForwarding : IServerEntryPoint + { + private readonly IServerApplicationHost _appHost; + private readonly ILogger _logger; + private readonly IHttpClient _httpClient; + private readonly IServerConfigurationManager _config; + private readonly IDeviceDiscovery _deviceDiscovery; + + private ITimer _timer; + private bool _isStarted; + private readonly ITimerFactory _timerFactory; + + public ExternalPortForwarding(ILogManager logmanager, IServerApplicationHost appHost, IServerConfigurationManager config, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient, ITimerFactory timerFactory) + { + _logger = logmanager.GetLogger("PortMapper"); + _appHost = appHost; + _config = config; + _deviceDiscovery = deviceDiscovery; + _httpClient = httpClient; + _timerFactory = timerFactory; + } + + private string _lastConfigIdentifier; + private string GetConfigIdentifier() + { + var values = new List<string>(); + var config = _config.Configuration; + + values.Add(config.EnableUPnP.ToString()); + values.Add(config.PublicPort.ToString(CultureInfo.InvariantCulture)); + values.Add(_appHost.HttpPort.ToString(CultureInfo.InvariantCulture)); + values.Add(_appHost.HttpsPort.ToString(CultureInfo.InvariantCulture)); + values.Add(config.EnableHttps.ToString()); + values.Add(_appHost.EnableHttps.ToString()); + + return string.Join("|", values.ToArray(values.Count)); + } + + void _config_ConfigurationUpdated(object sender, EventArgs e) + { + if (!string.Equals(_lastConfigIdentifier, GetConfigIdentifier(), StringComparison.OrdinalIgnoreCase)) + { + if (_isStarted) + { + DisposeNat(); + } + + Run(); + } + } + + public void Run() + { + NatUtility.Logger = _logger; + NatUtility.HttpClient = _httpClient; + + if (_config.Configuration.EnableUPnP) + { + Start(); + } + + _config.ConfigurationUpdated -= _config_ConfigurationUpdated; + _config.ConfigurationUpdated += _config_ConfigurationUpdated; + } + + private void Start() + { + _logger.Debug("Starting NAT discovery"); + NatUtility.EnabledProtocols = new List<NatProtocol> + { + NatProtocol.Pmp + }; + NatUtility.DeviceFound += NatUtility_DeviceFound; + + // Mono.Nat does never rise this event. The event is there however it is useless. + // You could remove it with no risk. + NatUtility.DeviceLost += NatUtility_DeviceLost; + + + NatUtility.StartDiscovery(); + + _timer = _timerFactory.Create(ClearCreatedRules, null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); + + _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; + + _lastConfigIdentifier = GetConfigIdentifier(); + + _isStarted = true; + } + + private async void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e) + { + if (_disposed) + { + return; + } + + var info = e.Argument; + + string usn; + if (!info.Headers.TryGetValue("USN", out usn)) usn = string.Empty; + + string nt; + if (!info.Headers.TryGetValue("NT", out nt)) nt = string.Empty; + + // Filter device type + if (usn.IndexOf("WANIPConnection:", StringComparison.OrdinalIgnoreCase) == -1 && + nt.IndexOf("WANIPConnection:", StringComparison.OrdinalIgnoreCase) == -1 && + usn.IndexOf("WANPPPConnection:", StringComparison.OrdinalIgnoreCase) == -1 && + nt.IndexOf("WANPPPConnection:", StringComparison.OrdinalIgnoreCase) == -1) + { + return; + } + + var identifier = string.IsNullOrWhiteSpace(usn) ? nt : usn; + + if (info.Location == null) + { + return; + } + + lock (_usnsHandled) + { + if (_usnsHandled.Contains(identifier)) + { + return; + } + _usnsHandled.Add(identifier); + } + + _logger.Debug("Found NAT device: " + identifier); + + IPAddress address; + if (IPAddress.TryParse(info.Location.Host, out address)) + { + // The Handle method doesn't need the port + var endpoint = new IPEndPoint(address, info.Location.Port); + + IPAddress localAddress = null; + + try + { + var localAddressString = await _appHost.GetLocalApiUrl().ConfigureAwait(false); + + Uri uri; + if (Uri.TryCreate(localAddressString, UriKind.Absolute, out uri)) + { + localAddressString = uri.Host; + + if (!IPAddress.TryParse(localAddressString, out localAddress)) + { + return; + } + } + } + catch (Exception ex) + { + return; + } + + if (_disposed) + { + return; + } + + _logger.Debug("Calling Nat.Handle on " + identifier); + NatUtility.Handle(localAddress, info, endpoint, NatProtocol.Upnp); + } + } + + private void ClearCreatedRules(object state) + { + lock (_createdRules) + { + _createdRules.Clear(); + } + lock (_usnsHandled) + { + _usnsHandled.Clear(); + } + } + + void NatUtility_DeviceFound(object sender, DeviceEventArgs e) + { + if (_disposed) + { + return; + } + + try + { + var device = e.Device; + _logger.Debug("NAT device found: {0}", device.LocalAddress.ToString()); + + CreateRules(device); + } + catch + { + // I think it could be a good idea to log the exception because + // you are using permanent portmapping here (never expire) and that means that next time + // CreatePortMap is invoked it can fails with a 718-ConflictInMappingEntry or not. That depends + // on the router's upnp implementation (specs says it should fail however some routers don't do it) + // It also can fail with others like 727-ExternalPortOnlySupportsWildcard, 728-NoPortMapsAvailable + // and those errors (upnp errors) could be useful for diagnosting. + + // Commenting out because users are reporting problems out of our control + //_logger.ErrorException("Error creating port forwarding rules", ex); + } + } + + private List<string> _createdRules = new List<string>(); + private List<string> _usnsHandled = new List<string>(); + private async void CreateRules(INatDevice device) + { + if (_disposed) + { + throw new ObjectDisposedException("PortMapper"); + } + + // On some systems the device discovered event seems to fire repeatedly + // This check will help ensure we're not trying to port map the same device over and over + + var address = device.LocalAddress.ToString(); + + lock (_createdRules) + { + if (!_createdRules.Contains(address)) + { + _createdRules.Add(address); + } + else + { + return; + } + } + + var success = await CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort).ConfigureAwait(false); + + if (success) + { + await CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort).ConfigureAwait(false); + } + } + + private async Task<bool> CreatePortMap(INatDevice device, int privatePort, int publicPort) + { + _logger.Debug("Creating port map on port {0}", privatePort); + + try + { + await device.CreatePortMap(new Mapping(Protocol.Tcp, privatePort, publicPort) + { + Description = _appHost.Name + + }).ConfigureAwait(false); + + return true; + } + catch (Exception ex) + { + _logger.Error("Error creating port map: " + ex.Message); + + return false; + } + } + + // As I said before, this method will be never invoked. You can remove it. + void NatUtility_DeviceLost(object sender, DeviceEventArgs e) + { + var device = e.Device; + _logger.Debug("NAT device lost: {0}", device.LocalAddress.ToString()); + } + + private bool _disposed = false; + public void Dispose() + { + _disposed = true; + DisposeNat(); + } + + private void DisposeNat() + { + _logger.Debug("Stopping NAT discovery"); + + if (_timer != null) + { + _timer.Dispose(); + _timer = null; + } + + _deviceDiscovery.DeviceDiscovered -= _deviceDiscovery_DeviceDiscovered; + + try + { + // This is not a significant improvement + NatUtility.StopDiscovery(); + NatUtility.DeviceFound -= NatUtility_DeviceFound; + NatUtility.DeviceLost -= NatUtility_DeviceLost; + } + // Statements in try-block will no fail because StopDiscovery is a one-line + // method that was no chances to fail. + // public static void StopDiscovery () + // { + // searching.Reset(); + // } + // IMO you could remove the catch-block + catch (Exception ex) + { + _logger.ErrorException("Error stopping NAT Discovery", ex); + } + finally + { + _isStarted = false; + } + } + } +}
\ No newline at end of file diff --git a/Emby.Server.Implementations/EntryPoints/UsageEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UsageEntryPoint.cs index 9fbe06673..99d39ffe0 100644 --- a/Emby.Server.Implementations/EntryPoints/UsageEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UsageEntryPoint.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.EntryPoints { @@ -58,7 +59,7 @@ namespace Emby.Server.Implementations.EntryPoints session.ApplicationVersion }; - var key = string.Join("_", keys.ToArray()).GetMD5(); + var key = string.Join("_", keys.ToArray(keys.Count)).GetMD5(); _apps.GetOrAdd(key, guid => GetNewClientInfo(session)); } diff --git a/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs b/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs deleted file mode 100644 index cf9fdbb16..000000000 --- a/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs +++ /dev/null @@ -1,813 +0,0 @@ -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.FileOrganization; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; -using MediaBrowser.Model.FileOrganization; -using MediaBrowser.Model.Logging; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.Library; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.IO; -using MediaBrowser.Model.IO; -using MediaBrowser.Naming.TV; -using EpisodeInfo = MediaBrowser.Controller.Providers.EpisodeInfo; - -namespace Emby.Server.Implementations.FileOrganization -{ - public class EpisodeFileOrganizer - { - private readonly ILibraryMonitor _libraryMonitor; - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; - private readonly IFileOrganizationService _organizationService; - private readonly IServerConfigurationManager _config; - private readonly IProviderManager _providerManager; - - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - public EpisodeFileOrganizer(IFileOrganizationService organizationService, IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager) - { - _organizationService = organizationService; - _config = config; - _fileSystem = fileSystem; - _logger = logger; - _libraryManager = libraryManager; - _libraryMonitor = libraryMonitor; - _providerManager = providerManager; - } - - public async Task<FileOrganizationResult> OrganizeEpisodeFile(string path, AutoOrganizeOptions options, bool overwriteExisting, CancellationToken cancellationToken) - { - _logger.Info("Sorting file {0}", path); - - var result = new FileOrganizationResult - { - Date = DateTime.UtcNow, - OriginalPath = path, - OriginalFileName = Path.GetFileName(path), - Type = FileOrganizerType.Episode, - FileSize = _fileSystem.GetFileInfo(path).Length - }; - - try - { - if (_libraryMonitor.IsPathLocked(path)) - { - result.Status = FileSortingStatus.Failure; - result.StatusMessage = "Path is locked by other processes. Please try again later."; - return result; - } - - var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); - var resolver = new EpisodeResolver(namingOptions, new NullLogger()); - - var episodeInfo = resolver.Resolve(path, false) ?? - new MediaBrowser.Naming.TV.EpisodeInfo(); - - var seriesName = episodeInfo.SeriesName; - - if (!string.IsNullOrEmpty(seriesName)) - { - var seasonNumber = episodeInfo.SeasonNumber; - - result.ExtractedSeasonNumber = seasonNumber; - - // Passing in true will include a few extra regex's - var episodeNumber = episodeInfo.EpisodeNumber; - - result.ExtractedEpisodeNumber = episodeNumber; - - var premiereDate = episodeInfo.IsByDate ? - new DateTime(episodeInfo.Year.Value, episodeInfo.Month.Value, episodeInfo.Day.Value) : - (DateTime?)null; - - if (episodeInfo.IsByDate || (seasonNumber.HasValue && episodeNumber.HasValue)) - { - if (episodeInfo.IsByDate) - { - _logger.Debug("Extracted information from {0}. Series name {1}, Date {2}", path, seriesName, premiereDate.Value); - } - else - { - _logger.Debug("Extracted information from {0}. Series name {1}, Season {2}, Episode {3}", path, seriesName, seasonNumber, episodeNumber); - } - - var endingEpisodeNumber = episodeInfo.EndingEpsiodeNumber; - - result.ExtractedEndingEpisodeNumber = endingEpisodeNumber; - - await OrganizeEpisode(path, - seriesName, - seasonNumber, - episodeNumber, - endingEpisodeNumber, - premiereDate, - options, - overwriteExisting, - false, - result, - cancellationToken).ConfigureAwait(false); - } - else - { - var msg = string.Format("Unable to determine episode number from {0}", path); - result.Status = FileSortingStatus.Failure; - result.StatusMessage = msg; - _logger.Warn(msg); - } - } - else - { - var msg = string.Format("Unable to determine series name from {0}", path); - result.Status = FileSortingStatus.Failure; - result.StatusMessage = msg; - _logger.Warn(msg); - } - - var previousResult = _organizationService.GetResultBySourcePath(path); - - if (previousResult != null) - { - // Don't keep saving the same result over and over if nothing has changed - if (previousResult.Status == result.Status && previousResult.StatusMessage == result.StatusMessage && result.Status != FileSortingStatus.Success) - { - return previousResult; - } - } - - await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - result.Status = FileSortingStatus.Failure; - result.StatusMessage = ex.Message; - } - - return result; - } - - public async Task<FileOrganizationResult> OrganizeWithCorrection(EpisodeFileOrganizationRequest request, AutoOrganizeOptions options, CancellationToken cancellationToken) - { - var result = _organizationService.GetResult(request.ResultId); - - try - { - Series series = null; - - if (request.NewSeriesProviderIds.Count > 0) - { - // We're having a new series here - SeriesInfo seriesRequest = new SeriesInfo(); - seriesRequest.ProviderIds = request.NewSeriesProviderIds; - - var refreshOptions = new MetadataRefreshOptions(_fileSystem); - series = new Series(); - series.Id = Guid.NewGuid(); - series.Name = request.NewSeriesName; - - int year; - if (int.TryParse(request.NewSeriesYear, out year)) - { - series.ProductionYear = year; - } - - var seriesFolderName = series.Name; - if (series.ProductionYear.HasValue) - { - seriesFolderName = string.Format("{0} ({1})", seriesFolderName, series.ProductionYear); - } - - seriesFolderName = _fileSystem.GetValidFilename(seriesFolderName); - - series.Path = Path.Combine(request.TargetFolder, seriesFolderName); - - series.ProviderIds = request.NewSeriesProviderIds; - - await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); - } - - if (series == null) - { - // Existing Series - series = (Series)_libraryManager.GetItemById(new Guid(request.SeriesId)); - } - - await OrganizeEpisode(result.OriginalPath, - series, - request.SeasonNumber, - request.EpisodeNumber, - request.EndingEpisodeNumber, - null, - options, - true, - request.RememberCorrection, - result, - cancellationToken).ConfigureAwait(false); - - await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - result.Status = FileSortingStatus.Failure; - result.StatusMessage = ex.Message; - } - - return result; - } - - private Task OrganizeEpisode(string sourcePath, - string seriesName, - int? seasonNumber, - int? episodeNumber, - int? endingEpiosdeNumber, - DateTime? premiereDate, - AutoOrganizeOptions options, - bool overwriteExisting, - bool rememberCorrection, - FileOrganizationResult result, - CancellationToken cancellationToken) - { - var series = GetMatchingSeries(seriesName, result, options); - - if (series == null) - { - var msg = string.Format("Unable to find series in library matching name {0}", seriesName); - result.Status = FileSortingStatus.Failure; - result.StatusMessage = msg; - _logger.Warn(msg); - return Task.FromResult(true); - } - - return OrganizeEpisode(sourcePath, - series, - seasonNumber, - episodeNumber, - endingEpiosdeNumber, - premiereDate, - options, - overwriteExisting, - rememberCorrection, - result, - cancellationToken); - } - - private async Task OrganizeEpisode(string sourcePath, - Series series, - int? seasonNumber, - int? episodeNumber, - int? endingEpiosdeNumber, - DateTime? premiereDate, - AutoOrganizeOptions options, - bool overwriteExisting, - bool rememberCorrection, - FileOrganizationResult result, - CancellationToken cancellationToken) - { - _logger.Info("Sorting file {0} into series {1}", sourcePath, series.Path); - - var originalExtractedSeriesString = result.ExtractedName; - - bool isNew = string.IsNullOrWhiteSpace(result.Id); - - if (isNew) - { - await _organizationService.SaveResult(result, cancellationToken); - } - - if (!_organizationService.AddToInProgressList(result, isNew)) - { - throw new Exception("File is currently processed otherwise. Please try again later."); - } - - try - { - // Proceed to sort the file - var newPath = await GetNewPath(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, premiereDate, options.TvOptions, cancellationToken).ConfigureAwait(false); - - if (string.IsNullOrEmpty(newPath)) - { - var msg = string.Format("Unable to sort {0} because target path could not be determined.", sourcePath); - throw new Exception(msg); - } - - _logger.Info("Sorting file {0} to new path {1}", sourcePath, newPath); - result.TargetPath = newPath; - - var fileExists = _fileSystem.FileExists(result.TargetPath); - var otherDuplicatePaths = GetOtherDuplicatePaths(result.TargetPath, series, seasonNumber, episodeNumber, endingEpiosdeNumber); - - if (!overwriteExisting) - { - if (options.TvOptions.CopyOriginalFile && fileExists && IsSameEpisode(sourcePath, newPath)) - { - var msg = string.Format("File '{0}' already copied to new path '{1}', stopping organization", sourcePath, newPath); - _logger.Info(msg); - result.Status = FileSortingStatus.SkippedExisting; - result.StatusMessage = msg; - return; - } - - if (fileExists) - { - var msg = string.Format("File '{0}' already exists as '{1}', stopping organization", sourcePath, newPath); - _logger.Info(msg); - result.Status = FileSortingStatus.SkippedExisting; - result.StatusMessage = msg; - result.TargetPath = newPath; - return; - } - - if (otherDuplicatePaths.Count > 0) - { - var msg = string.Format("File '{0}' already exists as these:'{1}'. Stopping organization", sourcePath, string.Join("', '", otherDuplicatePaths)); - _logger.Info(msg); - result.Status = FileSortingStatus.SkippedExisting; - result.StatusMessage = msg; - result.DuplicatePaths = otherDuplicatePaths; - return; - } - } - - PerformFileSorting(options.TvOptions, result); - - if (overwriteExisting) - { - var hasRenamedFiles = false; - - foreach (var path in otherDuplicatePaths) - { - _logger.Debug("Removing duplicate episode {0}", path); - - _libraryMonitor.ReportFileSystemChangeBeginning(path); - - var renameRelatedFiles = !hasRenamedFiles && - string.Equals(_fileSystem.GetDirectoryName(path), _fileSystem.GetDirectoryName(result.TargetPath), StringComparison.OrdinalIgnoreCase); - - if (renameRelatedFiles) - { - hasRenamedFiles = true; - } - - try - { - DeleteLibraryFile(path, renameRelatedFiles, result.TargetPath); - } - catch (IOException ex) - { - _logger.ErrorException("Error removing duplicate episode", ex, path); - } - finally - { - _libraryMonitor.ReportFileSystemChangeComplete(path, true); - } - } - } - } - catch (Exception ex) - { - result.Status = FileSortingStatus.Failure; - result.StatusMessage = ex.Message; - _logger.Warn(ex.Message); - return; - } - finally - { - _organizationService.RemoveFromInprogressList(result); - } - - if (rememberCorrection) - { - SaveSmartMatchString(originalExtractedSeriesString, series, options); - } - } - - private void SaveSmartMatchString(string matchString, Series series, AutoOrganizeOptions options) - { - if (string.IsNullOrEmpty(matchString) || matchString.Length < 3) - { - return; - } - - SmartMatchInfo info = options.SmartMatchInfos.FirstOrDefault(i => string.Equals(i.ItemName, series.Name, StringComparison.OrdinalIgnoreCase)); - - if (info == null) - { - info = new SmartMatchInfo(); - info.ItemName = series.Name; - info.OrganizerType = FileOrganizerType.Episode; - info.DisplayName = series.Name; - var list = options.SmartMatchInfos.ToList(); - list.Add(info); - options.SmartMatchInfos = list.ToArray(); - } - - if (!info.MatchStrings.Contains(matchString, StringComparer.OrdinalIgnoreCase)) - { - var list = info.MatchStrings.ToList(); - list.Add(matchString); - info.MatchStrings = list.ToArray(); - _config.SaveAutoOrganizeOptions(options); - } - } - - private void DeleteLibraryFile(string path, bool renameRelatedFiles, string targetPath) - { - _fileSystem.DeleteFile(path); - - if (!renameRelatedFiles) - { - return; - } - - // Now find other files - var originalFilenameWithoutExtension = Path.GetFileNameWithoutExtension(path); - var directory = _fileSystem.GetDirectoryName(path); - - if (!string.IsNullOrWhiteSpace(originalFilenameWithoutExtension) && !string.IsNullOrWhiteSpace(directory)) - { - // Get all related files, e.g. metadata, images, etc - var files = _fileSystem.GetFilePaths(directory) - .Where(i => (Path.GetFileNameWithoutExtension(i) ?? string.Empty).StartsWith(originalFilenameWithoutExtension, StringComparison.OrdinalIgnoreCase)) - .ToList(); - - var targetFilenameWithoutExtension = Path.GetFileNameWithoutExtension(targetPath); - - foreach (var file in files) - { - directory = _fileSystem.GetDirectoryName(file); - var filename = Path.GetFileName(file); - - filename = filename.Replace(originalFilenameWithoutExtension, targetFilenameWithoutExtension, - StringComparison.OrdinalIgnoreCase); - - var destination = Path.Combine(directory, filename); - - _fileSystem.MoveFile(file, destination); - } - } - } - - private List<string> GetOtherDuplicatePaths(string targetPath, - Series series, - int? seasonNumber, - int? episodeNumber, - int? endingEpisodeNumber) - { - // TODO: Support date-naming? - if (!seasonNumber.HasValue || !episodeNumber.HasValue) - { - return new List<string>(); - } - - var episodePaths = series.GetRecursiveChildren(i => i is Episode) - .OfType<Episode>() - .Where(i => - { - var locationType = i.LocationType; - - // Must be file system based and match exactly - if (locationType != LocationType.Remote && - locationType != LocationType.Virtual && - i.ParentIndexNumber.HasValue && - i.ParentIndexNumber.Value == seasonNumber && - i.IndexNumber.HasValue && - i.IndexNumber.Value == episodeNumber) - { - - if (endingEpisodeNumber.HasValue || i.IndexNumberEnd.HasValue) - { - return endingEpisodeNumber.HasValue && i.IndexNumberEnd.HasValue && - endingEpisodeNumber.Value == i.IndexNumberEnd.Value; - } - - return true; - } - - return false; - }) - .Select(i => i.Path) - .ToList(); - - var folder = _fileSystem.GetDirectoryName(targetPath); - var targetFileNameWithoutExtension = _fileSystem.GetFileNameWithoutExtension(targetPath); - - try - { - var filesOfOtherExtensions = _fileSystem.GetFilePaths(folder) - .Where(i => _libraryManager.IsVideoFile(i) && string.Equals(_fileSystem.GetFileNameWithoutExtension(i), targetFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)); - - episodePaths.AddRange(filesOfOtherExtensions); - } - catch (IOException) - { - // No big deal. Maybe the season folder doesn't already exist. - } - - return episodePaths.Where(i => !string.Equals(i, targetPath, StringComparison.OrdinalIgnoreCase)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToList(); - } - - private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizationResult result) - { - // We should probably handle this earlier so that we never even make it this far - if (string.Equals(result.OriginalPath, result.TargetPath, StringComparison.OrdinalIgnoreCase)) - { - return; - } - - _libraryMonitor.ReportFileSystemChangeBeginning(result.TargetPath); - - _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(result.TargetPath)); - - var targetAlreadyExists = _fileSystem.FileExists(result.TargetPath); - - try - { - if (targetAlreadyExists || options.CopyOriginalFile) - { - _fileSystem.CopyFile(result.OriginalPath, result.TargetPath, true); - } - else - { - _fileSystem.MoveFile(result.OriginalPath, result.TargetPath); - } - - result.Status = FileSortingStatus.Success; - result.StatusMessage = string.Empty; - } - catch (Exception ex) - { - var errorMsg = string.Format("Failed to move file from {0} to {1}: {2}", result.OriginalPath, result.TargetPath, ex.Message); - - result.Status = FileSortingStatus.Failure; - result.StatusMessage = errorMsg; - _logger.ErrorException(errorMsg, ex); - - return; - } - finally - { - _libraryMonitor.ReportFileSystemChangeComplete(result.TargetPath, true); - } - - if (targetAlreadyExists && !options.CopyOriginalFile) - { - try - { - _fileSystem.DeleteFile(result.OriginalPath); - } - catch (Exception ex) - { - _logger.ErrorException("Error deleting {0}", ex, result.OriginalPath); - } - } - } - - private Series GetMatchingSeries(string seriesName, FileOrganizationResult result, AutoOrganizeOptions options) - { - var parsedName = _libraryManager.ParseName(seriesName); - - var yearInName = parsedName.Year; - var nameWithoutYear = parsedName.Name; - - result.ExtractedName = nameWithoutYear; - result.ExtractedYear = yearInName; - - var series = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { typeof(Series).Name }, - Recursive = true, - DtoOptions = new DtoOptions(true) - }) - .Cast<Series>() - .Select(i => NameUtils.GetMatchScore(nameWithoutYear, yearInName, i)) - .Where(i => i.Item2 > 0) - .OrderByDescending(i => i.Item2) - .Select(i => i.Item1) - .FirstOrDefault(); - - if (series == null) - { - SmartMatchInfo info = options.SmartMatchInfos.FirstOrDefault(e => e.MatchStrings.Contains(nameWithoutYear, StringComparer.OrdinalIgnoreCase)); - - if (info != null) - { - series = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { typeof(Series).Name }, - Recursive = true, - Name = info.ItemName, - DtoOptions = new DtoOptions(true) - - }).Cast<Series>().FirstOrDefault(); - } - } - - return series; - } - - /// <summary> - /// Gets the new path. - /// </summary> - /// <param name="sourcePath">The source path.</param> - /// <param name="series">The series.</param> - /// <param name="seasonNumber">The season number.</param> - /// <param name="episodeNumber">The episode number.</param> - /// <param name="endingEpisodeNumber">The ending episode number.</param> - /// <param name="premiereDate">The premiere date.</param> - /// <param name="options">The options.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>System.String.</returns> - private async Task<string> GetNewPath(string sourcePath, - Series series, - int? seasonNumber, - int? episodeNumber, - int? endingEpisodeNumber, - DateTime? premiereDate, - TvFileOrganizationOptions options, - CancellationToken cancellationToken) - { - var episodeInfo = new EpisodeInfo - { - IndexNumber = episodeNumber, - IndexNumberEnd = endingEpisodeNumber, - MetadataCountryCode = series.GetPreferredMetadataCountryCode(), - MetadataLanguage = series.GetPreferredMetadataLanguage(), - ParentIndexNumber = seasonNumber, - SeriesProviderIds = series.ProviderIds, - PremiereDate = premiereDate - }; - - var searchResults = await _providerManager.GetRemoteSearchResults<Episode, EpisodeInfo>(new RemoteSearchQuery<EpisodeInfo> - { - SearchInfo = episodeInfo - - }, cancellationToken).ConfigureAwait(false); - - var episode = searchResults.FirstOrDefault(); - - if (episode == null) - { - var msg = string.Format("No provider metadata found for {0} season {1} episode {2}", series.Name, seasonNumber, episodeNumber); - _logger.Warn(msg); - throw new Exception(msg); - } - - var episodeName = episode.Name; - - //if (string.IsNullOrWhiteSpace(episodeName)) - //{ - // var msg = string.Format("No provider metadata found for {0} season {1} episode {2}", series.Name, seasonNumber, episodeNumber); - // _logger.Warn(msg); - // return null; - //} - - seasonNumber = seasonNumber ?? episode.ParentIndexNumber; - episodeNumber = episodeNumber ?? episode.IndexNumber; - - var newPath = GetSeasonFolderPath(series, seasonNumber.Value, options); - - var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber.Value, episodeNumber.Value, endingEpisodeNumber, episodeName, options); - - if (string.IsNullOrEmpty(episodeFileName)) - { - // cause failure - return string.Empty; - } - - newPath = Path.Combine(newPath, episodeFileName); - - return newPath; - } - - /// <summary> - /// Gets the season folder path. - /// </summary> - /// <param name="series">The series.</param> - /// <param name="seasonNumber">The season number.</param> - /// <param name="options">The options.</param> - /// <returns>System.String.</returns> - private string GetSeasonFolderPath(Series series, int seasonNumber, TvFileOrganizationOptions options) - { - // If there's already a season folder, use that - var season = series - .GetRecursiveChildren(i => i is Season && i.LocationType == LocationType.FileSystem && i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber) - .FirstOrDefault(); - - if (season != null) - { - return season.Path; - } - - var path = series.Path; - - if (series.ContainsEpisodesWithoutSeasonFolders) - { - return path; - } - - if (seasonNumber == 0) - { - return Path.Combine(path, _fileSystem.GetValidFilename(options.SeasonZeroFolderName)); - } - - var seasonFolderName = options.SeasonFolderPattern - .Replace("%s", seasonNumber.ToString(_usCulture)) - .Replace("%0s", seasonNumber.ToString("00", _usCulture)) - .Replace("%00s", seasonNumber.ToString("000", _usCulture)); - - return Path.Combine(path, _fileSystem.GetValidFilename(seasonFolderName)); - } - - private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options) - { - seriesName = _fileSystem.GetValidFilename(seriesName).Trim(); - - if (string.IsNullOrWhiteSpace(episodeTitle)) - { - episodeTitle = string.Empty; - } - else - { - episodeTitle = _fileSystem.GetValidFilename(episodeTitle).Trim(); - } - - var sourceExtension = (Path.GetExtension(sourcePath) ?? string.Empty).TrimStart('.'); - - var pattern = endingEpisodeNumber.HasValue ? options.MultiEpisodeNamePattern : options.EpisodeNamePattern; - - if (string.IsNullOrWhiteSpace(pattern)) - { - throw new Exception("GetEpisodeFileName: Configured episode name pattern is empty!"); - } - - var result = pattern.Replace("%sn", seriesName) - .Replace("%s.n", seriesName.Replace(" ", ".")) - .Replace("%s_n", seriesName.Replace(" ", "_")) - .Replace("%s", seasonNumber.ToString(_usCulture)) - .Replace("%0s", seasonNumber.ToString("00", _usCulture)) - .Replace("%00s", seasonNumber.ToString("000", _usCulture)) - .Replace("%ext", sourceExtension) - .Replace("%en", "%#1") - .Replace("%e.n", "%#2") - .Replace("%e_n", "%#3"); - - if (endingEpisodeNumber.HasValue) - { - result = result.Replace("%ed", endingEpisodeNumber.Value.ToString(_usCulture)) - .Replace("%0ed", endingEpisodeNumber.Value.ToString("00", _usCulture)) - .Replace("%00ed", endingEpisodeNumber.Value.ToString("000", _usCulture)); - } - - result = result.Replace("%e", episodeNumber.ToString(_usCulture)) - .Replace("%0e", episodeNumber.ToString("00", _usCulture)) - .Replace("%00e", episodeNumber.ToString("000", _usCulture)); - - if (result.Contains("%#")) - { - result = result.Replace("%#1", episodeTitle) - .Replace("%#2", episodeTitle.Replace(" ", ".")) - .Replace("%#3", episodeTitle.Replace(" ", "_")); - } - - // Finally, call GetValidFilename again in case user customized the episode expression with any invalid filename characters - return _fileSystem.GetValidFilename(result).Trim(); - } - - private bool IsSameEpisode(string sourcePath, string newPath) - { - try - { - var sourceFileInfo = _fileSystem.GetFileInfo(sourcePath); - var destinationFileInfo = _fileSystem.GetFileInfo(newPath); - - if (sourceFileInfo.Length == destinationFileInfo.Length) - { - return true; - } - } - catch (FileNotFoundException) - { - return false; - } - catch (IOException) - { - return false; - } - - return false; - } - } -} diff --git a/Emby.Server.Implementations/FileOrganization/Extensions.cs b/Emby.Server.Implementations/FileOrganization/Extensions.cs deleted file mode 100644 index 506bc0327..000000000 --- a/Emby.Server.Implementations/FileOrganization/Extensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.FileOrganization; -using System.Collections.Generic; - -namespace Emby.Server.Implementations.FileOrganization -{ - public static class ConfigurationExtension - { - public static AutoOrganizeOptions GetAutoOrganizeOptions(this IConfigurationManager manager) - { - return manager.GetConfiguration<AutoOrganizeOptions>("autoorganize"); - } - public static void SaveAutoOrganizeOptions(this IConfigurationManager manager, AutoOrganizeOptions options) - { - manager.SaveConfiguration("autoorganize", options); - } - } - - public class AutoOrganizeOptionsFactory : IConfigurationFactory - { - public IEnumerable<ConfigurationStore> GetConfigurations() - { - return new List<ConfigurationStore> - { - new ConfigurationStore - { - Key = "autoorganize", - ConfigurationType = typeof (AutoOrganizeOptions) - } - }; - } - } -} diff --git a/Emby.Server.Implementations/FileOrganization/FileOrganizationNotifier.cs b/Emby.Server.Implementations/FileOrganization/FileOrganizationNotifier.cs deleted file mode 100644 index 2a0176547..000000000 --- a/Emby.Server.Implementations/FileOrganization/FileOrganizationNotifier.cs +++ /dev/null @@ -1,80 +0,0 @@ -using MediaBrowser.Controller.FileOrganization; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.FileOrganization; -using MediaBrowser.Model.Logging; -using System; -using System.Threading; -using MediaBrowser.Model.Tasks; - -namespace Emby.Server.Implementations.FileOrganization -{ - /// <summary> - /// Class SessionInfoWebSocketListener - /// </summary> - class FileOrganizationNotifier : IServerEntryPoint - { - private readonly IFileOrganizationService _organizationService; - private readonly ISessionManager _sessionManager; - private readonly ITaskManager _taskManager; - - public FileOrganizationNotifier(ILogger logger, IFileOrganizationService organizationService, ISessionManager sessionManager, ITaskManager taskManager) - { - _organizationService = organizationService; - _sessionManager = sessionManager; - _taskManager = taskManager; - } - - public void Run() - { - _organizationService.ItemAdded += _organizationService_ItemAdded; - _organizationService.ItemRemoved += _organizationService_ItemRemoved; - _organizationService.ItemUpdated += _organizationService_ItemUpdated; - _organizationService.LogReset += _organizationService_LogReset; - - //_taskManager.TaskCompleted += _taskManager_TaskCompleted; - } - - private void _organizationService_LogReset(object sender, EventArgs e) - { - _sessionManager.SendMessageToAdminSessions("AutoOrganize_LogReset", (FileOrganizationResult)null, CancellationToken.None); - } - - private void _organizationService_ItemUpdated(object sender, GenericEventArgs<FileOrganizationResult> e) - { - _sessionManager.SendMessageToAdminSessions("AutoOrganize_ItemUpdated", e.Argument, CancellationToken.None); - } - - private void _organizationService_ItemRemoved(object sender, GenericEventArgs<FileOrganizationResult> e) - { - _sessionManager.SendMessageToAdminSessions("AutoOrganize_ItemRemoved", e.Argument, CancellationToken.None); - } - - private void _organizationService_ItemAdded(object sender, GenericEventArgs<FileOrganizationResult> e) - { - _sessionManager.SendMessageToAdminSessions("AutoOrganize_ItemAdded", e.Argument, CancellationToken.None); - } - - //private void _taskManager_TaskCompleted(object sender, TaskCompletionEventArgs e) - //{ - // var taskWithKey = e.Task.ScheduledTask as IHasKey; - // if (taskWithKey != null && taskWithKey.Key == "AutoOrganize") - // { - // _sessionManager.SendMessageToAdminSessions("AutoOrganize_TaskCompleted", (FileOrganizationResult)null, CancellationToken.None); - // } - //} - - public void Dispose() - { - _organizationService.ItemAdded -= _organizationService_ItemAdded; - _organizationService.ItemRemoved -= _organizationService_ItemRemoved; - _organizationService.ItemUpdated -= _organizationService_ItemUpdated; - _organizationService.LogReset -= _organizationService_LogReset; - - //_taskManager.TaskCompleted -= _taskManager_TaskCompleted; - } - - - } -} diff --git a/Emby.Server.Implementations/FileOrganization/FileOrganizationService.cs b/Emby.Server.Implementations/FileOrganization/FileOrganizationService.cs deleted file mode 100644 index d95bd8734..000000000 --- a/Emby.Server.Implementations/FileOrganization/FileOrganizationService.cs +++ /dev/null @@ -1,283 +0,0 @@ -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.FileOrganization; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.FileOrganization; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Querying; -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Events; -using MediaBrowser.Common.Events; - -using MediaBrowser.Controller.IO; -using MediaBrowser.Model.Tasks; - -namespace Emby.Server.Implementations.FileOrganization -{ - public class FileOrganizationService : IFileOrganizationService - { - private readonly ITaskManager _taskManager; - private readonly IFileOrganizationRepository _repo; - private readonly ILogger _logger; - private readonly ILibraryMonitor _libraryMonitor; - private readonly ILibraryManager _libraryManager; - private readonly IServerConfigurationManager _config; - private readonly IFileSystem _fileSystem; - private readonly IProviderManager _providerManager; - private readonly ConcurrentDictionary<string, bool> _inProgressItemIds = new ConcurrentDictionary<string, bool>(); - - public event EventHandler<GenericEventArgs<FileOrganizationResult>> ItemAdded; - public event EventHandler<GenericEventArgs<FileOrganizationResult>> ItemUpdated; - public event EventHandler<GenericEventArgs<FileOrganizationResult>> ItemRemoved; - public event EventHandler LogReset; - - public FileOrganizationService(ITaskManager taskManager, IFileOrganizationRepository repo, ILogger logger, ILibraryMonitor libraryMonitor, ILibraryManager libraryManager, IServerConfigurationManager config, IFileSystem fileSystem, IProviderManager providerManager) - { - _taskManager = taskManager; - _repo = repo; - _logger = logger; - _libraryMonitor = libraryMonitor; - _libraryManager = libraryManager; - _config = config; - _fileSystem = fileSystem; - _providerManager = providerManager; - } - - public void BeginProcessNewFiles() - { - _taskManager.CancelIfRunningAndQueue<OrganizerScheduledTask>(); - } - - public Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken) - { - if (result == null || string.IsNullOrEmpty(result.OriginalPath)) - { - throw new ArgumentNullException("result"); - } - - result.Id = result.OriginalPath.GetMD5().ToString("N"); - - return _repo.SaveResult(result, cancellationToken); - } - - public QueryResult<FileOrganizationResult> GetResults(FileOrganizationResultQuery query) - { - var results = _repo.GetResults(query); - - foreach (var result in results.Items) - { - result.IsInProgress = _inProgressItemIds.ContainsKey(result.Id); - } - - return results; - } - - public FileOrganizationResult GetResult(string id) - { - var result = _repo.GetResult(id); - - if (result != null) - { - result.IsInProgress = _inProgressItemIds.ContainsKey(result.Id); - } - - return result; - } - - public FileOrganizationResult GetResultBySourcePath(string path) - { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException("path"); - } - - var id = path.GetMD5().ToString("N"); - - return GetResult(id); - } - - public async Task DeleteOriginalFile(string resultId) - { - var result = _repo.GetResult(resultId); - - _logger.Info("Requested to delete {0}", result.OriginalPath); - - if (!AddToInProgressList(result, false)) - { - throw new Exception("Path is currently processed otherwise. Please try again later."); - } - - try - { - _fileSystem.DeleteFile(result.OriginalPath); - } - catch (Exception ex) - { - _logger.ErrorException("Error deleting {0}", ex, result.OriginalPath); - } - finally - { - RemoveFromInprogressList(result); - } - - await _repo.Delete(resultId); - - EventHelper.FireEventIfNotNull(ItemRemoved, this, new GenericEventArgs<FileOrganizationResult>(result), _logger); - } - - private AutoOrganizeOptions GetAutoOrganizeOptions() - { - return _config.GetAutoOrganizeOptions(); - } - - public async Task PerformOrganization(string resultId) - { - var result = _repo.GetResult(resultId); - - if (string.IsNullOrEmpty(result.TargetPath)) - { - throw new ArgumentException("No target path available."); - } - - var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager, - _libraryMonitor, _providerManager); - - var organizeResult = await organizer.OrganizeEpisodeFile(result.OriginalPath, GetAutoOrganizeOptions(), true, CancellationToken.None) - .ConfigureAwait(false); - - if (organizeResult.Status != FileSortingStatus.Success) - { - throw new Exception(result.StatusMessage); - } - } - - public async Task ClearLog() - { - await _repo.DeleteAll(); - EventHelper.FireEventIfNotNull(LogReset, this, EventArgs.Empty, _logger); - } - - public async Task PerformEpisodeOrganization(EpisodeFileOrganizationRequest request) - { - var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager, - _libraryMonitor, _providerManager); - - var result = await organizer.OrganizeWithCorrection(request, GetAutoOrganizeOptions(), CancellationToken.None).ConfigureAwait(false); - - if (result.Status != FileSortingStatus.Success) - { - throw new Exception(result.StatusMessage); - } - } - - public QueryResult<SmartMatchInfo> GetSmartMatchInfos(FileOrganizationResultQuery query) - { - if (query == null) - { - throw new ArgumentNullException("query"); - } - - var options = GetAutoOrganizeOptions(); - - var items = options.SmartMatchInfos.Skip(query.StartIndex ?? 0).Take(query.Limit ?? Int32.MaxValue).ToArray(); - - return new QueryResult<SmartMatchInfo>() - { - Items = items, - TotalRecordCount = options.SmartMatchInfos.Length - }; - } - - public void DeleteSmartMatchEntry(string itemName, string matchString) - { - if (string.IsNullOrEmpty(itemName)) - { - throw new ArgumentNullException("itemName"); - } - - if (string.IsNullOrEmpty(matchString)) - { - throw new ArgumentNullException("matchString"); - } - - var options = GetAutoOrganizeOptions(); - - SmartMatchInfo info = options.SmartMatchInfos.FirstOrDefault(i => string.Equals(i.ItemName, itemName)); - - if (info != null && info.MatchStrings.Contains(matchString)) - { - var list = info.MatchStrings.ToList(); - list.Remove(matchString); - info.MatchStrings = list.ToArray(); - - if (info.MatchStrings.Length == 0) - { - var infos = options.SmartMatchInfos.ToList(); - infos.Remove(info); - options.SmartMatchInfos = infos.ToArray(); - } - - _config.SaveAutoOrganizeOptions(options); - } - } - - /// <summary> - /// Attempts to add a an item to the list of currently processed items. - /// </summary> - /// <param name="result">The result item.</param> - /// <param name="isNewItem">Passing true will notify the client to reload all items, otherwise only a single item will be refreshed.</param> - /// <returns>True if the item was added, False if the item is already contained in the list.</returns> - public bool AddToInProgressList(FileOrganizationResult result, bool isNewItem) - { - if (string.IsNullOrWhiteSpace(result.Id)) - { - result.Id = result.OriginalPath.GetMD5().ToString("N"); - } - - if (!_inProgressItemIds.TryAdd(result.Id, false)) - { - return false; - } - - result.IsInProgress = true; - - if (isNewItem) - { - EventHelper.FireEventIfNotNull(ItemAdded, this, new GenericEventArgs<FileOrganizationResult>(result), _logger); - } - else - { - EventHelper.FireEventIfNotNull(ItemUpdated, this, new GenericEventArgs<FileOrganizationResult>(result), _logger); - } - - return true; - } - - /// <summary> - /// Removes an item from the list of currently processed items. - /// </summary> - /// <param name="result">The result item.</param> - /// <returns>True if the item was removed, False if the item was not contained in the list.</returns> - public bool RemoveFromInprogressList(FileOrganizationResult result) - { - bool itemValue; - var retval = _inProgressItemIds.TryRemove(result.Id, out itemValue); - - result.IsInProgress = false; - - EventHelper.FireEventIfNotNull(ItemUpdated, this, new GenericEventArgs<FileOrganizationResult>(result), _logger); - - return retval; - } - - } -} diff --git a/Emby.Server.Implementations/FileOrganization/NameUtils.cs b/Emby.Server.Implementations/FileOrganization/NameUtils.cs deleted file mode 100644 index eb22ca4ea..000000000 --- a/Emby.Server.Implementations/FileOrganization/NameUtils.cs +++ /dev/null @@ -1,81 +0,0 @@ -using MediaBrowser.Model.Extensions; -using MediaBrowser.Controller.Entities; -using System; -using System.Globalization; -using MediaBrowser.Controller.Extensions; - -namespace Emby.Server.Implementations.FileOrganization -{ - public static class NameUtils - { - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - internal static Tuple<T, int> GetMatchScore<T>(string sortedName, int? year, T series) - where T : BaseItem - { - var score = 0; - - var seriesNameWithoutYear = series.Name; - if (series.ProductionYear.HasValue) - { - seriesNameWithoutYear = seriesNameWithoutYear.Replace(series.ProductionYear.Value.ToString(UsCulture), String.Empty); - } - - if (IsNameMatch(sortedName, seriesNameWithoutYear)) - { - score++; - - if (year.HasValue && series.ProductionYear.HasValue) - { - if (year.Value == series.ProductionYear.Value) - { - score++; - } - else - { - // Regardless of name, return a 0 score if the years don't match - return new Tuple<T, int>(series, 0); - } - } - } - - return new Tuple<T, int>(series, score); - } - - - private static bool IsNameMatch(string name1, string name2) - { - name1 = GetComparableName(name1); - name2 = GetComparableName(name2); - - return String.Equals(name1, name2, StringComparison.OrdinalIgnoreCase); - } - - private static string GetComparableName(string name) - { - name = name.RemoveDiacritics(); - - name = " " + name + " "; - - name = name.Replace(".", " ") - .Replace("_", " ") - .Replace(" and ", " ") - .Replace(".and.", " ") - .Replace("&", " ") - .Replace("!", " ") - .Replace("(", " ") - .Replace(")", " ") - .Replace(":", " ") - .Replace(",", " ") - .Replace("-", " ") - .Replace("'", " ") - .Replace("[", " ") - .Replace("]", " ") - .Replace(" a ", String.Empty, StringComparison.OrdinalIgnoreCase) - .Replace(" the ", String.Empty, StringComparison.OrdinalIgnoreCase) - .Replace(" ", String.Empty); - - return name.Trim(); - } - } -} diff --git a/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs b/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs deleted file mode 100644 index b71a3975f..000000000 --- a/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs +++ /dev/null @@ -1,101 +0,0 @@ -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.FileOrganization; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.FileOrganization; -using MediaBrowser.Model.Logging; -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -using MediaBrowser.Controller.IO; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Tasks; - -namespace Emby.Server.Implementations.FileOrganization -{ - public class OrganizerScheduledTask : IScheduledTask, IConfigurableScheduledTask - { - private readonly ILibraryMonitor _libraryMonitor; - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; - private readonly IServerConfigurationManager _config; - private readonly IFileOrganizationService _organizationService; - private readonly IProviderManager _providerManager; - - public OrganizerScheduledTask(ILibraryMonitor libraryMonitor, ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, IServerConfigurationManager config, IFileOrganizationService organizationService, IProviderManager providerManager) - { - _libraryMonitor = libraryMonitor; - _libraryManager = libraryManager; - _logger = logger; - _fileSystem = fileSystem; - _config = config; - _organizationService = organizationService; - _providerManager = providerManager; - } - - public string Name - { - get { return "Organize new media files"; } - } - - public string Description - { - get { return "Processes new files available in the configured watch folder."; } - } - - public string Category - { - get { return "Library"; } - } - - private AutoOrganizeOptions GetAutoOrganizeOptions() - { - return _config.GetAutoOrganizeOptions(); - } - - public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress) - { - if (GetAutoOrganizeOptions().TvOptions.IsEnabled) - { - await new TvFolderOrganizer(_libraryManager, _logger, _fileSystem, _libraryMonitor, _organizationService, _config, _providerManager) - .Organize(GetAutoOrganizeOptions(), cancellationToken, progress).ConfigureAwait(false); - } - } - - /// <summary> - /// Creates the triggers that define when the task will run - /// </summary> - /// <returns>IEnumerable{BaseTaskTrigger}.</returns> - public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() - { - return new[] { - - // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromMinutes(5).Ticks} - }; - } - - public bool IsHidden - { - get { return !GetAutoOrganizeOptions().TvOptions.IsEnabled; } - } - - public bool IsEnabled - { - get { return GetAutoOrganizeOptions().TvOptions.IsEnabled; } - } - - public bool IsLogged - { - get { return false; } - } - - public string Key - { - get { return "AutoOrganize"; } - } - } -} diff --git a/Emby.Server.Implementations/FileOrganization/TvFolderOrganizer.cs b/Emby.Server.Implementations/FileOrganization/TvFolderOrganizer.cs deleted file mode 100644 index 0dbd6f837..000000000 --- a/Emby.Server.Implementations/FileOrganization/TvFolderOrganizer.cs +++ /dev/null @@ -1,236 +0,0 @@ -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.FileOrganization; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.FileOrganization; -using MediaBrowser.Model.Logging; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using MediaBrowser.Controller.IO; -using MediaBrowser.Model.IO; - -namespace Emby.Server.Implementations.FileOrganization -{ - public class TvFolderOrganizer - { - private readonly ILibraryMonitor _libraryMonitor; - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; - private readonly IFileOrganizationService _organizationService; - private readonly IServerConfigurationManager _config; - private readonly IProviderManager _providerManager; - - public TvFolderOrganizer(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, ILibraryMonitor libraryMonitor, IFileOrganizationService organizationService, IServerConfigurationManager config, IProviderManager providerManager) - { - _libraryManager = libraryManager; - _logger = logger; - _fileSystem = fileSystem; - _libraryMonitor = libraryMonitor; - _organizationService = organizationService; - _config = config; - _providerManager = providerManager; - } - - private bool EnableOrganization(FileSystemMetadata fileInfo, TvFileOrganizationOptions options) - { - var minFileBytes = options.MinFileSizeMb * 1024 * 1024; - - try - { - return _libraryManager.IsVideoFile(fileInfo.FullName) && fileInfo.Length >= minFileBytes; - } - catch (Exception ex) - { - _logger.ErrorException("Error organizing file {0}", ex, fileInfo.Name); - } - - return false; - } - - private bool IsValidWatchLocation(string path, List<string> libraryFolderPaths) - { - if (IsPathAlreadyInMediaLibrary(path, libraryFolderPaths)) - { - _logger.Info("Folder {0} is not eligible for auto-organize because it is also part of an Emby library", path); - return false; - } - - return true; - } - - private bool IsPathAlreadyInMediaLibrary(string path, List<string> libraryFolderPaths) - { - return libraryFolderPaths.Any(i => string.Equals(i, path, StringComparison.Ordinal) || _fileSystem.ContainsSubPath(i, path)); - } - - public async Task Organize(AutoOrganizeOptions options, CancellationToken cancellationToken, IProgress<double> progress) - { - var libraryFolderPaths = _libraryManager.GetVirtualFolders().SelectMany(i => i.Locations).ToList(); - - var watchLocations = options.TvOptions.WatchLocations - .Where(i => IsValidWatchLocation(i, libraryFolderPaths)) - .ToList(); - - var eligibleFiles = watchLocations.SelectMany(GetFilesToOrganize) - .OrderBy(_fileSystem.GetCreationTimeUtc) - .Where(i => EnableOrganization(i, options.TvOptions)) - .ToList(); - - var processedFolders = new HashSet<string>(); - - progress.Report(10); - - if (eligibleFiles.Count > 0) - { - var numComplete = 0; - - foreach (var file in eligibleFiles) - { - cancellationToken.ThrowIfCancellationRequested(); - - var organizer = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, - _libraryMonitor, _providerManager); - - try - { - var result = await organizer.OrganizeEpisodeFile(file.FullName, options, options.TvOptions.OverwriteExistingEpisodes, cancellationToken).ConfigureAwait(false); - - if (result.Status == FileSortingStatus.Success && !processedFolders.Contains(file.DirectoryName, StringComparer.OrdinalIgnoreCase)) - { - processedFolders.Add(file.DirectoryName); - } - } - catch (OperationCanceledException) - { - break; - } - catch (Exception ex) - { - _logger.ErrorException("Error organizing episode {0}", ex, file.FullName); - } - - numComplete++; - double percent = numComplete; - percent /= eligibleFiles.Count; - - progress.Report(10 + 89 * percent); - } - } - - cancellationToken.ThrowIfCancellationRequested(); - progress.Report(99); - - foreach (var path in processedFolders) - { - var deleteExtensions = options.TvOptions.LeftOverFileExtensionsToDelete - .Select(i => i.Trim().TrimStart('.')) - .Where(i => !string.IsNullOrEmpty(i)) - .Select(i => "." + i) - .ToList(); - - if (deleteExtensions.Count > 0) - { - DeleteLeftOverFiles(path, deleteExtensions); - } - - if (options.TvOptions.DeleteEmptyFolders) - { - if (!IsWatchFolder(path, watchLocations)) - { - DeleteEmptyFolders(path); - } - } - } - - progress.Report(100); - } - - /// <summary> - /// Gets the files to organize. - /// </summary> - /// <param name="path">The path.</param> - /// <returns>IEnumerable{FileInfo}.</returns> - private List<FileSystemMetadata> GetFilesToOrganize(string path) - { - try - { - return _fileSystem.GetFiles(path, true) - .ToList(); - } - catch (IOException ex) - { - _logger.ErrorException("Error getting files from {0}", ex, path); - - return new List<FileSystemMetadata>(); - } - } - - /// <summary> - /// Deletes the left over files. - /// </summary> - /// <param name="path">The path.</param> - /// <param name="extensions">The extensions.</param> - private void DeleteLeftOverFiles(string path, IEnumerable<string> extensions) - { - var eligibleFiles = _fileSystem.GetFilePaths(path, extensions.ToArray(), false, true) - .ToList(); - - foreach (var file in eligibleFiles) - { - try - { - _fileSystem.DeleteFile(file); - } - catch (Exception ex) - { - _logger.ErrorException("Error deleting file {0}", ex, file); - } - } - } - - /// <summary> - /// Deletes the empty folders. - /// </summary> - /// <param name="path">The path.</param> - private void DeleteEmptyFolders(string path) - { - try - { - foreach (var d in _fileSystem.GetDirectoryPaths(path)) - { - DeleteEmptyFolders(d); - } - - var entries = _fileSystem.GetFileSystemEntryPaths(path); - - if (!entries.Any()) - { - try - { - _logger.Debug("Deleting empty directory {0}", path); - _fileSystem.DeleteDirectory(path, false); - } - catch (UnauthorizedAccessException) { } - catch (IOException) { } - } - } - catch (UnauthorizedAccessException) { } - } - - /// <summary> - /// Determines if a given folder path is contained in a folder list - /// </summary> - /// <param name="path">The folder path to check.</param> - /// <param name="watchLocations">A list of folders.</param> - private bool IsWatchFolder(string path, IEnumerable<string> watchLocations) - { - return watchLocations.Contains(path, StringComparer.OrdinalIgnoreCase); - } - } -}
\ No newline at end of file diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 05f78eba9..f150e4785 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -125,13 +125,6 @@ namespace Emby.Server.Implementations.HttpServer return _appHost.CreateInstance(type); } - private ServiceController CreateServiceController() - { - var types = _restServices.Select(r => r.GetType()).ToArray(); - - return new ServiceController(() => types); - } - /// <summary> /// Applies the request filters. Returns whether or not the request has been handled /// and no more processing should be done. @@ -186,7 +179,7 @@ namespace Emby.Server.Implementations.HttpServer attributes.Sort((x, y) => x.Priority - y.Priority); - return attributes.ToArray(); + return attributes.ToArray(attributes.Count); } /// <summary> @@ -697,11 +690,13 @@ namespace Emby.Server.Implementations.HttpServer { _restServices.AddRange(services); - ServiceController = CreateServiceController(); + ServiceController = new ServiceController(); _logger.Info("Calling ServiceStack AppHost.Init"); - ServiceController.Init(this); + var types = _restServices.Select(r => r.GetType()).ToArray(); + + ServiceController.Init(this, types); var requestFilters = _appHost.GetExports<IRequestFilter>().ToList(); foreach (var filter in requestFilters) @@ -741,7 +736,7 @@ namespace Emby.Server.Implementations.HttpServer }); } - return routes.ToArray(); + return routes.ToArray(routes.Count); } public Func<string, object> GetParseFn(Type propertyType) diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index 396bd8e88..7bd8fe2bf 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -6,19 +6,16 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.IO.Compression; using System.Net; using System.Runtime.Serialization; using System.Text; using System.Threading.Tasks; using System.Xml; -using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.Services; using MediaBrowser.Model.IO; using MediaBrowser.Model.Services; using IRequest = MediaBrowser.Model.Services.IRequest; using MimeTypes = MediaBrowser.Model.Net.MimeTypes; -using StreamWriter = Emby.Server.Implementations.HttpServer.StreamWriter; namespace Emby.Server.Implementations.HttpServer { @@ -193,50 +190,37 @@ namespace Emby.Server.Implementations.HttpServer /// <returns></returns> public object ToOptimizedResult<T>(IRequest request, T dto) { - var compressionType = GetCompressionType(request); - if (compressionType == null) - { - var contentType = request.ResponseContentType; - - switch (GetRealContentType(contentType)) - { - case "application/xml": - case "text/xml": - case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml - return SerializeToXmlString(dto); - - case "application/json": - case "text/json": - return _jsonSerializer.SerializeToString(dto); - } - } + var contentType = request.ResponseContentType; - // Do not use the memoryStreamFactory here, they don't place nice with compression - using (var ms = new MemoryStream()) + switch (GetRealContentType(contentType)) { - var contentType = request.ResponseContentType; - var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType); + case "application/xml": + case "text/xml": + case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml + return SerializeToXmlString(dto); - writerFn(dto, ms); + case "application/json": + case "text/json": + return _jsonSerializer.SerializeToString(dto); + default: + { + var ms = new MemoryStream(); + var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType); - ms.Position = 0; + writerFn(dto, ms); + + ms.Position = 0; - var responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + if (string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase)) + { + return GetHttpResult(new byte[] { }, contentType, true); + } - return GetCompressedResult(ms, compressionType, responseHeaders, false, request.ResponseContentType).Result; + return GetHttpResult(ms, contentType, true); + } } } - private static Stream GetCompressionStream(Stream outputStream, string compressionType) - { - if (compressionType == "deflate") - return new DeflateStream(outputStream, CompressionMode.Compress, true); - if (compressionType == "gzip") - return new GZipStream(outputStream, CompressionMode.Compress, true); - - throw new NotSupportedException(compressionType); - } - public static string GetRealContentType(string contentType) { return contentType == null @@ -568,123 +552,47 @@ namespace Emby.Server.Implementations.HttpServer var contentType = options.ContentType; var responseHeaders = options.ResponseHeaders; - var requestedCompressionType = GetCompressionType(requestContext); + //var requestedCompressionType = GetCompressionType(requestContext); - if (!compress || string.IsNullOrEmpty(requestedCompressionType)) - { - var rangeHeader = requestContext.Headers.Get("Range"); - - if (!isHeadRequest && !string.IsNullOrWhiteSpace(options.Path)) - { - return new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem) - { - OnComplete = options.OnComplete, - OnError = options.OnError, - FileShare = options.FileShare - }; - } - - if (!string.IsNullOrWhiteSpace(rangeHeader)) - { - var stream = await factoryFn().ConfigureAwait(false); - - return new RangeRequestWriter(rangeHeader, stream, contentType, isHeadRequest, _logger) - { - OnComplete = options.OnComplete - }; - } - else - { - var stream = await factoryFn().ConfigureAwait(false); - - responseHeaders["Content-Length"] = stream.Length.ToString(UsCulture); - - if (isHeadRequest) - { - stream.Dispose(); - - return GetHttpResult(new byte[] { }, contentType, true); - } - - return new StreamWriter(stream, contentType, _logger) - { - OnComplete = options.OnComplete, - OnError = options.OnError - }; - } - } + var rangeHeader = requestContext.Headers.Get("Range"); - using (var stream = await factoryFn().ConfigureAwait(false)) + if (!isHeadRequest && !string.IsNullOrWhiteSpace(options.Path)) { - return await GetCompressedResult(stream, requestedCompressionType, responseHeaders, isHeadRequest, contentType).ConfigureAwait(false); + return new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem) + { + OnComplete = options.OnComplete, + OnError = options.OnError, + FileShare = options.FileShare + }; } - } - private async Task<IHasHeaders> GetCompressedResult(Stream stream, - string requestedCompressionType, - IDictionary<string, string> responseHeaders, - bool isHeadRequest, - string contentType) - { - using (var reader = new MemoryStream()) + if (!string.IsNullOrWhiteSpace(rangeHeader)) { - await stream.CopyToAsync(reader).ConfigureAwait(false); - - reader.Position = 0; - var content = reader.ToArray(); + var stream = await factoryFn().ConfigureAwait(false); - if (content.Length >= 1024) + return new RangeRequestWriter(rangeHeader, stream, contentType, isHeadRequest, _logger) { - content = Compress(content, requestedCompressionType); - responseHeaders["Content-Encoding"] = requestedCompressionType; - } + OnComplete = options.OnComplete + }; + } + else + { + var stream = await factoryFn().ConfigureAwait(false); - responseHeaders["Vary"] = "Accept-Encoding"; - responseHeaders["Content-Length"] = content.Length.ToString(UsCulture); + responseHeaders["Content-Length"] = stream.Length.ToString(UsCulture); if (isHeadRequest) { + stream.Dispose(); + return GetHttpResult(new byte[] { }, contentType, true); } - return GetHttpResult(content, contentType, true, responseHeaders); - } - } - - private byte[] Compress(byte[] bytes, string compressionType) - { - if (compressionType == "deflate") - return Deflate(bytes); - - if (compressionType == "gzip") - return GZip(bytes); - - throw new NotSupportedException(compressionType); - } - - private byte[] Deflate(byte[] bytes) - { - // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream - // Which means we must use MemoryStream since you have to use ToArray() on a closed Stream - using (var ms = new MemoryStream()) - using (var zipStream = new DeflateStream(ms, CompressionMode.Compress)) - { - zipStream.Write(bytes, 0, bytes.Length); - zipStream.Dispose(); - - return ms.ToArray(); - } - } - - private byte[] GZip(byte[] buffer) - { - using (var ms = new MemoryStream()) - using (var zipStream = new GZipStream(ms, CompressionMode.Compress)) - { - zipStream.Write(buffer, 0, buffer.Length); - zipStream.Dispose(); - - return ms.ToArray(); + return new StreamWriter(stream, contentType, _logger) + { + OnComplete = options.OnComplete, + OnError = options.OnError + }; } } diff --git a/Emby.Server.Implementations/HttpServer/LoggerUtils.cs b/Emby.Server.Implementations/HttpServer/LoggerUtils.cs index f0e75eea4..de30dc30a 100644 --- a/Emby.Server.Implementations/HttpServer/LoggerUtils.cs +++ b/Emby.Server.Implementations/HttpServer/LoggerUtils.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using MediaBrowser.Model.Services; using SocketHttpListener.Net; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.HttpServer { @@ -29,7 +30,7 @@ namespace Emby.Server.Implementations.HttpServer } else { - var headerText = string.Join(", ", headers.Select(i => i.Name + "=" + i.Value).ToArray()); + var headerText = string.Join(", ", headers.Select(i => i.Name + "=" + i.Value).ToArray(headers.Count)); logger.Info("HTTP {0} {1}. {2}", method, url, headerText); } diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/HttpUtility.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/HttpUtility.cs index 4fbe0ed94..4e8dd7362 100644 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/HttpUtility.cs +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/HttpUtility.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Text; using MediaBrowser.Model.Services; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.HttpServer.SocketSharp { @@ -585,7 +586,7 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp WriteCharBytes(bytes, ch, e); } - byte[] buf = bytes.ToArray(); + byte[] buf = bytes.ToArray(bytes.Count); bytes = null; return e.GetString(buf, 0, buf.Length); diff --git a/Emby.Server.Implementations/HttpServerFactory.cs b/Emby.Server.Implementations/HttpServerFactory.cs new file mode 100644 index 000000000..b1d78e6f4 --- /dev/null +++ b/Emby.Server.Implementations/HttpServerFactory.cs @@ -0,0 +1,110 @@ +using System; +using System.IO; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Emby.Common.Implementations.Net; +using Emby.Server.Implementations.HttpServer; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.System; +using MediaBrowser.Model.Text; +using ServiceStack.Text.Jsv; +using SocketHttpListener.Primitives; + +namespace Emby.Server.Implementations +{ + /// <summary> + /// Class ServerFactory + /// </summary> + public static class HttpServerFactory + { + /// <summary> + /// Creates the server. + /// </summary> + /// <returns>IHttpServer.</returns> + public static IHttpServer CreateServer(IServerApplicationHost applicationHost, + ILogManager logManager, + IServerConfigurationManager config, + INetworkManager networkmanager, + IMemoryStreamFactory streamProvider, + string serverName, + string defaultRedirectpath, + ITextEncoding textEncoding, + ISocketFactory socketFactory, + ICryptoProvider cryptoProvider, + IJsonSerializer json, + IXmlSerializer xml, + IEnvironmentInfo environment, + ICertificate certificate, + IFileSystem fileSystem, + bool enableDualModeSockets) + { + var logger = logManager.GetLogger("HttpServer"); + + return new HttpListenerHost(applicationHost, + logger, + config, + serverName, + defaultRedirectpath, + networkmanager, + streamProvider, + textEncoding, + socketFactory, + cryptoProvider, + json, + xml, + environment, + certificate, + new StreamFactory(), + GetParseFn, + enableDualModeSockets, + fileSystem); + } + + private static Func<string, object> GetParseFn(Type propertyType) + { + return s => JsvReader.GetParseFn(propertyType)(s); + } + } + + public class StreamFactory : IStreamFactory + { + public Stream CreateNetworkStream(IAcceptSocket acceptSocket, bool ownsSocket) + { + var netSocket = (NetAcceptSocket)acceptSocket; + + return new SocketStream(netSocket.Socket, ownsSocket); + } + + public Task AuthenticateSslStreamAsServer(Stream stream, ICertificate certificate) + { + var sslStream = (SslStream)stream; + var cert = (Certificate)certificate; + + return sslStream.AuthenticateAsServerAsync(cert.X509Certificate); + } + + public Stream CreateSslStream(Stream innerStream, bool leaveInnerStreamOpen) + { + return new SslStream(innerStream, leaveInnerStreamOpen); + } + } + + public class Certificate : ICertificate + { + public Certificate(X509Certificate x509Certificate) + { + X509Certificate = x509Certificate; + } + + public X509Certificate X509Certificate { get; private set; } + } +} diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index d4914e734..b2554049d 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -130,14 +130,6 @@ namespace Emby.Server.Implementations.IO paths = _affectedPaths.ToList(); } - // Extend the timer as long as any of the paths are still being written to. - if (paths.Any(IsFileLocked)) - { - Logger.Info("Timer extended."); - RestartTimer(); - return; - } - Logger.Debug("Timer stopped."); DisposeTimer(); @@ -229,90 +221,6 @@ namespace Emby.Server.Implementations.IO return item; } - private bool IsFileLocked(string path) - { - if (_environmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows) - { - // Causing lockups on linux - return false; - } - - // Only try to open video files - if (!_libraryManager.IsVideoFile(path)) - { - return false; - } - - try - { - var data = _fileSystem.GetFileSystemInfo(path); - - if (!data.Exists - || data.IsDirectory - - // Opening a writable stream will fail with readonly files - || data.IsReadOnly) - { - return false; - } - } - catch (IOException) - { - return false; - } - catch (Exception ex) - { - Logger.ErrorException("Error getting file system info for: {0}", ex, path); - return false; - } - - // In order to determine if the file is being written to, we have to request write access - // But if the server only has readonly access, this is going to cause this entire algorithm to fail - // So we'll take a best guess about our access level - //var requestedFileAccess = ConfigurationManager.Configuration.SaveLocalMeta - // ? FileAccessMode.ReadWrite - // : FileAccessMode.Read; - - var requestedFileAccess = FileAccessMode.Read; - try - { - using (_fileSystem.GetFileStream(path, FileOpenMode.Open, requestedFileAccess, FileShareMode.ReadWrite)) - { - //file is not locked - return false; - } - } - catch (DirectoryNotFoundException) - { - // File may have been deleted - return false; - } - catch (FileNotFoundException) - { - // File may have been deleted - return false; - } - catch (UnauthorizedAccessException) - { - Logger.Debug("No write permission for: {0}.", path); - return false; - } - catch (IOException) - { - //the file is unavailable because it is: - //still being written to - //or being processed by another thread - //or does not exist (has already been processed) - Logger.Debug("{0} is locked.", path); - return true; - } - catch (Exception ex) - { - Logger.ErrorException("Error determining if file is locked: {0}", ex, path); - return false; - } - } - private void DisposeTimer() { lock (_timerLock) diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs new file mode 100644 index 000000000..c452c01be --- /dev/null +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -0,0 +1,652 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.System; +using MediaBrowser.Model.Tasks; +using MediaBrowser.Model.Threading; + +namespace Emby.Server.Implementations.IO +{ + public class LibraryMonitor : ILibraryMonitor + { + /// <summary> + /// The file system watchers + /// </summary> + private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new ConcurrentDictionary<string, FileSystemWatcher>(StringComparer.OrdinalIgnoreCase); + /// <summary> + /// The affected paths + /// </summary> + private readonly List<FileRefresher> _activeRefreshers = new List<FileRefresher>(); + + /// <summary> + /// A dynamic list of paths that should be ignored. Added to during our own file sytem modifications. + /// </summary> + private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase); + + /// <summary> + /// Any file name ending in any of these will be ignored by the watchers + /// </summary> + private readonly IReadOnlyList<string> _alwaysIgnoreFiles = new List<string> + { + "small.jpg", + "albumart.jpg", + + // WMC temp recording directories that will constantly be written to + "TempRec", + "TempSBE" + }; + + private readonly IReadOnlyList<string> _alwaysIgnoreSubstrings = new List<string> + { + // Synology + "eaDir", + "#recycle", + ".wd_tv", + ".actors" + }; + + private readonly IReadOnlyList<string> _alwaysIgnoreExtensions = new List<string> + { + // thumbs.db + ".db", + + // bts sync files + ".bts", + ".sync" + }; + + /// <summary> + /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope. + /// </summary> + /// <param name="path">The path.</param> + private void TemporarilyIgnore(string path) + { + _tempIgnoredPaths[path] = path; + } + + public void ReportFileSystemChangeBeginning(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + TemporarilyIgnore(path); + } + + public bool IsPathLocked(string path) + { + var lockedPaths = _tempIgnoredPaths.Keys.ToList(); + return lockedPaths.Any(i => _fileSystem.AreEqual(i, path) || _fileSystem.ContainsSubPath(i, path)); + } + + public async void ReportFileSystemChangeComplete(string path, bool refreshPath) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + // This is an arbitraty amount of time, but delay it because file system writes often trigger events long after the file was actually written to. + // Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds + // But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata + await Task.Delay(45000).ConfigureAwait(false); + + string val; + _tempIgnoredPaths.TryRemove(path, out val); + + if (refreshPath) + { + try + { + ReportFileSystemChanged(path); + } + catch (Exception ex) + { + Logger.ErrorException("Error in ReportFileSystemChanged for {0}", ex, path); + } + } + } + + /// <summary> + /// Gets or sets the logger. + /// </summary> + /// <value>The logger.</value> + private ILogger Logger { get; set; } + + /// <summary> + /// Gets or sets the task manager. + /// </summary> + /// <value>The task manager.</value> + private ITaskManager TaskManager { get; set; } + + private ILibraryManager LibraryManager { get; set; } + private IServerConfigurationManager ConfigurationManager { get; set; } + + private readonly IFileSystem _fileSystem; + private readonly ITimerFactory _timerFactory; + private readonly IEnvironmentInfo _environmentInfo; + + /// <summary> + /// Initializes a new instance of the <see cref="LibraryMonitor" /> class. + /// </summary> + public LibraryMonitor(ILogManager logManager, ITaskManager taskManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ITimerFactory timerFactory, ISystemEvents systemEvents, IEnvironmentInfo environmentInfo) + { + if (taskManager == null) + { + throw new ArgumentNullException("taskManager"); + } + + LibraryManager = libraryManager; + TaskManager = taskManager; + Logger = logManager.GetLogger(GetType().Name); + ConfigurationManager = configurationManager; + _fileSystem = fileSystem; + _timerFactory = timerFactory; + _environmentInfo = environmentInfo; + + systemEvents.Resume += _systemEvents_Resume; + } + + private void _systemEvents_Resume(object sender, EventArgs e) + { + Restart(); + } + + private void Restart() + { + Stop(); + + if (!_disposed) + { + Start(); + } + } + + private bool IsLibraryMonitorEnabaled(BaseItem item) + { + if (item is BasePluginFolder) + { + return false; + } + + var options = LibraryManager.GetLibraryOptions(item); + + if (options != null) + { + return options.EnableRealtimeMonitor; + } + + return false; + } + + public void Start() + { + LibraryManager.ItemAdded += LibraryManager_ItemAdded; + LibraryManager.ItemRemoved += LibraryManager_ItemRemoved; + + var pathsToWatch = new List<string> { }; + + var paths = LibraryManager + .RootFolder + .Children + .Where(IsLibraryMonitorEnabaled) + .OfType<Folder>() + .SelectMany(f => f.PhysicalLocations) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(i => i) + .ToList(); + + foreach (var path in paths) + { + if (!ContainsParentFolder(pathsToWatch, path)) + { + pathsToWatch.Add(path); + } + } + + foreach (var path in pathsToWatch) + { + StartWatchingPath(path); + } + } + + private void StartWatching(BaseItem item) + { + if (IsLibraryMonitorEnabaled(item)) + { + StartWatchingPath(item.Path); + } + } + + /// <summary> + /// Handles the ItemRemoved event of the LibraryManager control. + /// </summary> + /// <param name="sender">The source of the event.</param> + /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param> + void LibraryManager_ItemRemoved(object sender, ItemChangeEventArgs e) + { + if (e.Item.GetParent() is AggregateFolder) + { + StopWatchingPath(e.Item.Path); + } + } + + /// <summary> + /// Handles the ItemAdded event of the LibraryManager control. + /// </summary> + /// <param name="sender">The source of the event.</param> + /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param> + void LibraryManager_ItemAdded(object sender, ItemChangeEventArgs e) + { + if (e.Item.GetParent() is AggregateFolder) + { + StartWatching(e.Item); + } + } + + /// <summary> + /// Examine a list of strings assumed to be file paths to see if it contains a parent of + /// the provided path. + /// </summary> + /// <param name="lst">The LST.</param> + /// <param name="path">The path.</param> + /// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns> + /// <exception cref="System.ArgumentNullException">path</exception> + private static bool ContainsParentFolder(IEnumerable<string> lst, string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentNullException("path"); + } + + path = path.TrimEnd(Path.DirectorySeparatorChar); + + return lst.Any(str => + { + //this should be a little quicker than examining each actual parent folder... + var compare = str.TrimEnd(Path.DirectorySeparatorChar); + + return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar); + }); + } + + /// <summary> + /// Starts the watching path. + /// </summary> + /// <param name="path">The path.</param> + private void StartWatchingPath(string path) + { + if (!_fileSystem.DirectoryExists(path)) + { + // Seeing a crash in the mono runtime due to an exception being thrown on a different thread + Logger.Info("Skipping realtime monitor for {0} because the path does not exist", path); + return; + } + + if (_environmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows) + { + if (path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase) || path.StartsWith("smb://", StringComparison.OrdinalIgnoreCase)) + { + // not supported + return; + } + } + + // Already being watched + if (_fileSystemWatchers.ContainsKey(path)) + { + return; + } + + // Creating a FileSystemWatcher over the LAN can take hundreds of milliseconds, so wrap it in a Task to do them all in parallel + Task.Run(() => + { + try + { + var newWatcher = new FileSystemWatcher(path, "*") + { + IncludeSubdirectories = true + }; + + if (_environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows) + { + newWatcher.InternalBufferSize = 32767; + } + + newWatcher.NotifyFilter = NotifyFilters.CreationTime | + NotifyFilters.DirectoryName | + NotifyFilters.FileName | + NotifyFilters.LastWrite | + NotifyFilters.Size | + NotifyFilters.Attributes; + + newWatcher.Created += watcher_Changed; + + // Seeing mono crashes on background threads we can't catch, testing if this might help + if (_environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows) + { + newWatcher.Deleted += watcher_Changed; + } + + newWatcher.Renamed += watcher_Changed; + newWatcher.Changed += watcher_Changed; + + newWatcher.Error += watcher_Error; + + if (_fileSystemWatchers.TryAdd(path, newWatcher)) + { + newWatcher.EnableRaisingEvents = true; + Logger.Info("Watching directory " + path); + } + else + { + newWatcher.Dispose(); + } + + } + catch (Exception ex) + { + Logger.ErrorException("Error watching path: {0}", ex, path); + } + }); + } + + /// <summary> + /// Stops the watching path. + /// </summary> + /// <param name="path">The path.</param> + private void StopWatchingPath(string path) + { + FileSystemWatcher watcher; + + if (_fileSystemWatchers.TryGetValue(path, out watcher)) + { + DisposeWatcher(watcher); + } + } + + /// <summary> + /// Disposes the watcher. + /// </summary> + /// <param name="watcher">The watcher.</param> + private void DisposeWatcher(FileSystemWatcher watcher) + { + try + { + using (watcher) + { + Logger.Info("Stopping directory watching for path {0}", watcher.Path); + + watcher.EnableRaisingEvents = false; + } + } + catch + { + + } + finally + { + RemoveWatcherFromList(watcher); + } + } + + /// <summary> + /// Removes the watcher from list. + /// </summary> + /// <param name="watcher">The watcher.</param> + private void RemoveWatcherFromList(FileSystemWatcher watcher) + { + FileSystemWatcher removed; + + _fileSystemWatchers.TryRemove(watcher.Path, out removed); + } + + /// <summary> + /// Handles the Error event of the watcher control. + /// </summary> + /// <param name="sender">The source of the event.</param> + /// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param> + void watcher_Error(object sender, ErrorEventArgs e) + { + var ex = e.GetException(); + var dw = (FileSystemWatcher)sender; + + Logger.ErrorException("Error in Directory watcher for: " + dw.Path, ex); + + DisposeWatcher(dw); + } + + /// <summary> + /// Handles the Changed event of the watcher control. + /// </summary> + /// <param name="sender">The source of the event.</param> + /// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param> + void watcher_Changed(object sender, FileSystemEventArgs e) + { + try + { + //Logger.Debug("Changed detected of type " + e.ChangeType + " to " + e.FullPath); + + var path = e.FullPath; + + ReportFileSystemChanged(path); + } + catch (Exception ex) + { + Logger.ErrorException("Exception in ReportFileSystemChanged. Path: {0}", ex, e.FullPath); + } + } + + public void ReportFileSystemChanged(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + var filename = Path.GetFileName(path); + + var monitorPath = !string.IsNullOrEmpty(filename) && + !_alwaysIgnoreFiles.Contains(filename, StringComparer.OrdinalIgnoreCase) && + !_alwaysIgnoreExtensions.Contains(Path.GetExtension(path) ?? string.Empty, StringComparer.OrdinalIgnoreCase) && + _alwaysIgnoreSubstrings.All(i => path.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1); + + // Ignore certain files + var tempIgnorePaths = _tempIgnoredPaths.Keys.ToList(); + + // If the parent of an ignored path has a change event, ignore that too + if (tempIgnorePaths.Any(i => + { + if (_fileSystem.AreEqual(i, path)) + { + Logger.Debug("Ignoring change to {0}", path); + return true; + } + + if (_fileSystem.ContainsSubPath(i, path)) + { + Logger.Debug("Ignoring change to {0}", path); + return true; + } + + // Go up a level + var parent = _fileSystem.GetDirectoryName(i); + if (!string.IsNullOrEmpty(parent)) + { + if (_fileSystem.AreEqual(parent, path)) + { + Logger.Debug("Ignoring change to {0}", path); + return true; + } + } + + return false; + + })) + { + monitorPath = false; + } + + if (monitorPath) + { + // Avoid implicitly captured closure + CreateRefresher(path); + } + } + + private void CreateRefresher(string path) + { + var parentPath = _fileSystem.GetDirectoryName(path); + + lock (_activeRefreshers) + { + var refreshers = _activeRefreshers.ToList(); + foreach (var refresher in refreshers) + { + // Path is already being refreshed + if (_fileSystem.AreEqual(path, refresher.Path)) + { + refresher.RestartTimer(); + return; + } + + // Parent folder is already being refreshed + if (_fileSystem.ContainsSubPath(refresher.Path, path)) + { + refresher.AddPath(path); + return; + } + + // New path is a parent + if (_fileSystem.ContainsSubPath(path, refresher.Path)) + { + refresher.ResetPath(path, null); + return; + } + + // They are siblings. Rebase the refresher to the parent folder. + if (string.Equals(parentPath, _fileSystem.GetDirectoryName(refresher.Path), StringComparison.Ordinal)) + { + refresher.ResetPath(parentPath, path); + return; + } + } + + var newRefresher = new FileRefresher(path, _fileSystem, ConfigurationManager, LibraryManager, TaskManager, Logger, _timerFactory, _environmentInfo, LibraryManager); + newRefresher.Completed += NewRefresher_Completed; + _activeRefreshers.Add(newRefresher); + } + } + + private void NewRefresher_Completed(object sender, EventArgs e) + { + var refresher = (FileRefresher)sender; + DisposeRefresher(refresher); + } + + /// <summary> + /// Stops this instance. + /// </summary> + public void Stop() + { + LibraryManager.ItemAdded -= LibraryManager_ItemAdded; + LibraryManager.ItemRemoved -= LibraryManager_ItemRemoved; + + foreach (var watcher in _fileSystemWatchers.Values.ToList()) + { + watcher.Created -= watcher_Changed; + watcher.Deleted -= watcher_Changed; + watcher.Renamed -= watcher_Changed; + watcher.Changed -= watcher_Changed; + + try + { + watcher.EnableRaisingEvents = false; + } + catch (InvalidOperationException) + { + // Seeing this under mono on linux sometimes + // Collection was modified; enumeration operation may not execute. + } + + watcher.Dispose(); + } + + _fileSystemWatchers.Clear(); + DisposeRefreshers(); + } + + private void DisposeRefresher(FileRefresher refresher) + { + lock (_activeRefreshers) + { + refresher.Dispose(); + _activeRefreshers.Remove(refresher); + } + } + + private void DisposeRefreshers() + { + lock (_activeRefreshers) + { + foreach (var refresher in _activeRefreshers.ToList()) + { + refresher.Dispose(); + } + _activeRefreshers.Clear(); + } + } + + private bool _disposed; + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() + { + _disposed = true; + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + Stop(); + } + } + } + + public class LibraryMonitorStartup : IServerEntryPoint + { + private readonly ILibraryMonitor _monitor; + + public LibraryMonitorStartup(ILibraryMonitor monitor) + { + _monitor = monitor; + } + + public void Run() + { + _monitor.Start(); + } + + public void Dispose() + { + } + } +} diff --git a/Emby.Server.Implementations/IO/MemoryStreamProvider.cs b/Emby.Server.Implementations/IO/MemoryStreamProvider.cs new file mode 100644 index 000000000..eca76203c --- /dev/null +++ b/Emby.Server.Implementations/IO/MemoryStreamProvider.cs @@ -0,0 +1,56 @@ +using System.IO; +using MediaBrowser.Model.IO; +using Microsoft.IO; + +namespace Emby.Server.Implementations.IO +{ + public class RecyclableMemoryStreamProvider : IMemoryStreamFactory + { + readonly RecyclableMemoryStreamManager _manager = new RecyclableMemoryStreamManager(); + + public MemoryStream CreateNew() + { + return _manager.GetStream(); + } + + public MemoryStream CreateNew(int capacity) + { + return _manager.GetStream("RecyclableMemoryStream", capacity); + } + + public MemoryStream CreateNew(byte[] buffer) + { + return _manager.GetStream("RecyclableMemoryStream", buffer, 0, buffer.Length); + } + + public bool TryGetBuffer(MemoryStream stream, out byte[] buffer) + { + buffer = stream.GetBuffer(); + return true; + } + } + + public class MemoryStreamProvider : IMemoryStreamFactory + { + public MemoryStream CreateNew() + { + return new MemoryStream(); + } + + public MemoryStream CreateNew(int capacity) + { + return new MemoryStream(capacity); + } + + public MemoryStream CreateNew(byte[] buffer) + { + return new MemoryStream(buffer); + } + + public bool TryGetBuffer(MemoryStream stream, out byte[] buffer) + { + buffer = stream.GetBuffer(); + return true; + } + } +} diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index b8ce23a42..3f9ea79c6 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -37,12 +37,12 @@ namespace Emby.Server.Implementations.Images ImageProcessor = imageProcessor; } - protected virtual bool Supports(IHasImages item) + protected virtual bool Supports(IHasMetadata item) { return true; } - public virtual IEnumerable<ImageType> GetSupportedImages(IHasImages item) + public virtual IEnumerable<ImageType> GetSupportedImages(IHasMetadata item) { return new List<ImageType> { @@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.Images }; } - private IEnumerable<ImageType> GetEnabledImages(IHasImages item) + private IEnumerable<ImageType> GetEnabledImages(IHasMetadata item) { //var options = ProviderManager.GetMetadataOptions(item); @@ -84,7 +84,7 @@ namespace Emby.Server.Implementations.Images return updateType; } - protected async Task<ItemUpdateType> FetchAsync(IHasImages item, ImageType imageType, MetadataRefreshOptions options, CancellationToken cancellationToken) + protected async Task<ItemUpdateType> FetchAsync(IHasMetadata item, ImageType imageType, MetadataRefreshOptions options, CancellationToken cancellationToken) { var image = item.GetImageInfo(imageType, 0); @@ -106,7 +106,7 @@ namespace Emby.Server.Implementations.Images return await FetchToFileInternal(item, items, imageType, cancellationToken).ConfigureAwait(false); } - protected async Task<ItemUpdateType> FetchToFileInternal(IHasImages item, + protected async Task<ItemUpdateType> FetchToFileInternal(IHasMetadata item, List<BaseItem> itemsWithImages, ImageType imageType, CancellationToken cancellationToken) @@ -132,14 +132,14 @@ namespace Emby.Server.Implementations.Images return ItemUpdateType.ImageUpdate; } - protected abstract List<BaseItem> GetItemsWithImages(IHasImages item); + protected abstract List<BaseItem> GetItemsWithImages(IHasMetadata item); - protected string CreateThumbCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath) + protected string CreateThumbCollage(IHasMetadata primaryItem, List<BaseItem> items, string outputPath) { return CreateCollage(primaryItem, items, outputPath, 640, 360); } - protected virtual IEnumerable<string> GetStripCollageImagePaths(IHasImages primaryItem, IEnumerable<BaseItem> items) + protected virtual IEnumerable<string> GetStripCollageImagePaths(IHasMetadata primaryItem, IEnumerable<BaseItem> items) { return items .Select(i => @@ -161,22 +161,22 @@ namespace Emby.Server.Implementations.Images .Where(i => !string.IsNullOrWhiteSpace(i)); } - protected string CreatePosterCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath) + protected string CreatePosterCollage(IHasMetadata primaryItem, List<BaseItem> items, string outputPath) { return CreateCollage(primaryItem, items, outputPath, 400, 600); } - protected string CreateSquareCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath) + protected string CreateSquareCollage(IHasMetadata primaryItem, List<BaseItem> items, string outputPath) { return CreateCollage(primaryItem, items, outputPath, 600, 600); } - protected string CreateThumbCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath, int width, int height) + protected string CreateThumbCollage(IHasMetadata primaryItem, List<BaseItem> items, string outputPath, int width, int height) { return CreateCollage(primaryItem, items, outputPath, width, height); } - private string CreateCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath, int width, int height) + private string CreateCollage(IHasMetadata primaryItem, List<BaseItem> items, string outputPath, int width, int height) { FileSystem.CreateDirectory(FileSystem.GetDirectoryName(outputPath)); @@ -207,7 +207,7 @@ namespace Emby.Server.Implementations.Images get { return "Dynamic Image Provider"; } } - protected virtual string CreateImage(IHasImages item, + protected virtual string CreateImage(IHasMetadata item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, @@ -267,7 +267,7 @@ namespace Emby.Server.Implementations.Images return false; } - protected bool HasChanged(IHasImages item, ImageType type) + protected bool HasChanged(IHasMetadata item, ImageType type) { var image = item.GetImageInfo(type, 0); @@ -293,20 +293,16 @@ namespace Emby.Server.Implementations.Images return true; } - protected List<BaseItem> GetFinalItems(List<BaseItem> items) + protected List<BaseItem> GetFinalItems(IEnumerable<BaseItem> items) { return GetFinalItems(items, 4); } - protected virtual List<BaseItem> GetFinalItems(List<BaseItem> items, int limit) + protected virtual List<BaseItem> GetFinalItems(IEnumerable<BaseItem> items, int limit) { - // Rotate the images once every x days - var random = DateTime.Now.DayOfYear % MaxImageAgeDays; - return items - .OrderBy(i => (random + string.Empty + items.IndexOf(i)).GetMD5()) + .OrderBy(i => Guid.NewGuid()) .Take(limit) - .OrderBy(i => i.Name) .ToList(); } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 4846a5768..40dccf9ba 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -136,14 +136,6 @@ namespace Emby.Server.Implementations.Library /// <value>The configuration manager.</value> private IServerConfigurationManager ConfigurationManager { get; set; } - /// <summary> - /// A collection of items that may be referenced from multiple physical places in the library - /// (typically, multiple user roots). We store them here and be sure they all reference a - /// single instance. - /// </summary> - /// <value>The by reference items.</value> - private ConcurrentDictionary<Guid, BaseItem> ByReferenceItems { get; set; } - private readonly Func<ILibraryMonitor> _libraryMonitorFactory; private readonly Func<IProviderManager> _providerManagerFactory; private readonly Func<IUserViewManager> _userviewManager; @@ -186,7 +178,6 @@ namespace Emby.Server.Implementations.Library _fileSystem = fileSystem; _providerManagerFactory = providerManagerFactory; _userviewManager = userviewManager; - ByReferenceItems = new ConcurrentDictionary<Guid, BaseItem>(); _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>(); ConfigurationManager.ConfigurationUpdated += ConfigurationUpdated; @@ -560,22 +551,6 @@ namespace Emby.Server.Implementations.Library return key.GetMD5(); } - /// <summary> - /// Ensure supplied item has only one instance throughout - /// </summary> - /// <param name="item">The item.</param> - /// <returns>The proper instance to the item</returns> - public BaseItem GetOrAddByReferenceItem(BaseItem item) - { - // Add this item to our list if not there already - if (!ByReferenceItems.TryAdd(item.Id, item)) - { - // Already there - return the existing reference - item = ByReferenceItems[item.Id]; - } - return item; - } - public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null) { @@ -1197,6 +1172,8 @@ namespace Emby.Server.Implementations.Library progress.Report(percent * 100); } + await ItemRepository.UpdateInheritedValues(cancellationToken).ConfigureAwait(false); + progress.Report(100); } @@ -1298,7 +1275,7 @@ namespace Emby.Server.Implementations.Library return item; } - public IEnumerable<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent) + public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent) { if (query.Recursive && query.ParentId.HasValue) { @@ -1317,7 +1294,7 @@ namespace Emby.Server.Implementations.Library return ItemRepository.GetItemList(query); } - public IEnumerable<BaseItem> GetItemList(InternalItemsQuery query) + public List<BaseItem> GetItemList(InternalItemsQuery query) { return GetItemList(query, true); } @@ -1341,7 +1318,7 @@ namespace Emby.Server.Implementations.Library return ItemRepository.GetCount(query); } - public IEnumerable<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents) + public List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents) { SetTopParentIdsOrAncestors(query, parents); @@ -1515,9 +1492,11 @@ namespace Emby.Server.Implementations.Library return ItemRepository.GetItems(query); } + var list = ItemRepository.GetItemList(query); + return new QueryResult<BaseItem> { - Items = ItemRepository.GetItemList(query).ToArray() + Items = list.ToArray(list.Count) }; } @@ -2395,8 +2374,7 @@ namespace Emby.Server.Implementations.Library var resolver = new EpisodeResolver(GetNamingOptions(), new NullLogger()); - var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd || - episode.VideoType == VideoType.HdDvd; + var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd; var locationType = episode.LocationType; @@ -2593,7 +2571,7 @@ namespace Emby.Server.Implementations.Library { var namingOptions = GetNamingOptions(); - var files = owner.DetectIsInMixedFolder() ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory) + var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory) .Where(i => string.Equals(i.Name, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase)) .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false)) .ToList(); @@ -2611,7 +2589,7 @@ namespace Emby.Server.Implementations.Library var resolvers = new IItemResolver[] { - new GenericVideoResolver<Trailer>(this) + new GenericVideoResolver<Trailer>(this, _fileSystem) }; return ResolvePaths(files, directoryService, null, new LibraryOptions(), null, resolvers) @@ -2632,7 +2610,7 @@ namespace Emby.Server.Implementations.Library return video; // Sort them so that the list can be easily compared for changes - }).OrderBy(i => i.Path).ToList(); + }).OrderBy(i => i.Path); } private static readonly string[] ExtrasSubfolderNames = new[] { "extras", "specials", "shorts", "scenes", "featurettes", "behind the scenes", "deleted scenes", "interviews" }; @@ -2674,7 +2652,7 @@ namespace Emby.Server.Implementations.Library return video; // Sort them so that the list can be easily compared for changes - }).OrderBy(i => i.Path).ToList(); + }).OrderBy(i => i.Path); } public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem) @@ -2857,7 +2835,7 @@ namespace Emby.Server.Implementations.Library return ItemRepository.UpdatePeople(item.Id, people); } - public async Task<ItemImageInfo> ConvertImageToLocal(IHasImages item, ItemImageInfo image, int imageIndex) + public async Task<ItemImageInfo> ConvertImageToLocal(IHasMetadata item, ItemImageInfo image, int imageIndex) { foreach (var url in image.Path.Split('|')) { diff --git a/Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs b/Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs index e64980dff..757e67eb4 100644 --- a/Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs +++ b/Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs @@ -3,6 +3,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -45,7 +46,7 @@ namespace Emby.Server.Implementations.Library Recursive = true, DtoOptions = new DtoOptions(false) - }).ToArray(); + }); var numComplete = 0; @@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.Library progress.Report(100); } - private async Task AssignTrailers(IHasTrailers item, BaseItem[] channelTrailers) + private async Task AssignTrailers(IHasTrailers item, IEnumerable<BaseItem> channelTrailers) { if (item is Game) { @@ -90,7 +91,7 @@ namespace Emby.Server.Implementations.Library }); var trailerIds = trailers.Select(i => i.Id) - .ToList(); + .ToArray(); if (!trailerIds.SequenceEqual(item.RemoteTrailerIds)) { diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index c1360c887..6e0489d88 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -49,10 +49,9 @@ namespace Emby.Server.Implementations.Library _providers = providers.ToArray(); } - public IEnumerable<MediaStream> GetMediaStreams(MediaStreamQuery query) + public List<MediaStream> GetMediaStreams(MediaStreamQuery query) { - var list = _itemRepo.GetMediaStreams(query) - .ToList(); + var list = _itemRepo.GetMediaStreams(query); foreach (var stream in list) { @@ -77,7 +76,7 @@ namespace Emby.Server.Implementations.Library return false; } - public IEnumerable<MediaStream> GetMediaStreams(string mediaSourceId) + public List<MediaStream> GetMediaStreams(string mediaSourceId) { var list = GetMediaStreams(new MediaStreamQuery { @@ -87,7 +86,7 @@ namespace Emby.Server.Implementations.Library return GetMediaStreamsForItem(list); } - public IEnumerable<MediaStream> GetMediaStreams(Guid itemId) + public List<MediaStream> GetMediaStreams(Guid itemId) { var list = GetMediaStreams(new MediaStreamQuery { @@ -97,7 +96,7 @@ namespace Emby.Server.Implementations.Library return GetMediaStreamsForItem(list); } - private IEnumerable<MediaStream> GetMediaStreamsForItem(IEnumerable<MediaStream> streams) + private List<MediaStream> GetMediaStreamsForItem(IEnumerable<MediaStream> streams) { var list = streams.ToList(); @@ -253,7 +252,7 @@ namespace Emby.Server.Implementations.Library return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)); } - public IEnumerable<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution, User user = null) + public List<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution, User user = null) { if (item == null) { @@ -265,7 +264,7 @@ namespace Emby.Server.Implementations.Library return item.GetMediaSources(enablePathSubstitution); } - var sources = item.GetMediaSources(enablePathSubstitution).ToList(); + var sources = item.GetMediaSources(enablePathSubstitution); if (user != null) { diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index f0d07cc3c..7f77170ad 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -19,27 +19,27 @@ namespace Emby.Server.Implementations.Library _libraryManager = libraryManager; } - public IEnumerable<Audio> GetInstantMixFromSong(Audio item, User user, DtoOptions dtoOptions) + public List<BaseItem> GetInstantMixFromSong(Audio item, User user, DtoOptions dtoOptions) { var list = new List<Audio> { item }; - return list.Concat(GetInstantMixFromGenres(item.Genres, user, dtoOptions)); + return list.Concat(GetInstantMixFromGenres(item.Genres, user, dtoOptions)).ToList(); } - public IEnumerable<Audio> GetInstantMixFromArtist(MusicArtist item, User user, DtoOptions dtoOptions) + public List<BaseItem> GetInstantMixFromArtist(MusicArtist item, User user, DtoOptions dtoOptions) { return GetInstantMixFromGenres(item.Genres, user, dtoOptions); } - public IEnumerable<Audio> GetInstantMixFromAlbum(MusicAlbum item, User user, DtoOptions dtoOptions) + public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User user, DtoOptions dtoOptions) { return GetInstantMixFromGenres(item.Genres, user, dtoOptions); } - public IEnumerable<Audio> GetInstantMixFromFolder(Folder item, User user, DtoOptions dtoOptions) + public List<BaseItem> GetInstantMixFromFolder(Folder item, User user, DtoOptions dtoOptions) { var genres = item .GetRecursiveChildren(user, new InternalItemsQuery(user) @@ -55,12 +55,12 @@ namespace Emby.Server.Implementations.Library return GetInstantMixFromGenres(genres, user, dtoOptions); } - public IEnumerable<Audio> GetInstantMixFromPlaylist(Playlist item, User user, DtoOptions dtoOptions) + public List<BaseItem> GetInstantMixFromPlaylist(Playlist item, User user, DtoOptions dtoOptions) { return GetInstantMixFromGenres(item.Genres, user, dtoOptions); } - public IEnumerable<Audio> GetInstantMixFromGenres(IEnumerable<string> genres, User user, DtoOptions dtoOptions) + public List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User user, DtoOptions dtoOptions) { var genreIds = genres.DistinctNames().Select(i => { @@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.Library return GetInstantMixFromGenreIds(genreIds, user, dtoOptions); } - public IEnumerable<Audio> GetInstantMixFromGenreIds(IEnumerable<string> genreIds, User user, DtoOptions dtoOptions) + public List<BaseItem> GetInstantMixFromGenreIds(IEnumerable<string> genreIds, User user, DtoOptions dtoOptions) { return _libraryManager.GetItemList(new InternalItemsQuery(user) { @@ -92,10 +92,10 @@ namespace Emby.Server.Implementations.Library DtoOptions = dtoOptions - }).Cast<Audio>(); + }); } - public IEnumerable<Audio> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions) + public List<BaseItem> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions) { var genre = item as MusicGenre; if (genre != null) @@ -133,7 +133,7 @@ namespace Emby.Server.Implementations.Library return GetInstantMixFromFolder(folder, user, dtoOptions); } - return new Audio[] { }; + return new List<BaseItem>(); } } } diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs index 24dc1104a..d0096de0c 100644 --- a/Emby.Server.Implementations/Library/ResolverHelper.cs +++ b/Emby.Server.Implementations/Library/ResolverHelper.cs @@ -46,7 +46,7 @@ namespace Emby.Server.Implementations.Library var fileInfo = directoryService.GetFile(item.Path); SetDateCreated(item, fileSystem, fileInfo); - EnsureName(item, fileInfo); + EnsureName(item, item.Path, fileInfo); } /// <summary> @@ -73,7 +73,7 @@ namespace Emby.Server.Implementations.Library item.Id = libraryManager.GetNewItemId(item.Path, item.GetType()); // Make sure the item has a name - EnsureName(item, args.FileInfo); + EnsureName(item, item.Path, args.FileInfo); item.IsLocked = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 || item.GetParents().Any(i => i.IsLocked); @@ -85,14 +85,14 @@ namespace Emby.Server.Implementations.Library /// <summary> /// Ensures the name. /// </summary> - /// <param name="item">The item.</param> - /// <param name="fileInfo">The file information.</param> - private static void EnsureName(BaseItem item, FileSystemMetadata fileInfo) + private static void EnsureName(BaseItem item, string fullPath, FileSystemMetadata fileInfo) { // If the subclass didn't supply a name, add it here - if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(item.Path)) + if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(fullPath)) { - item.Name = GetDisplayName(fileInfo.Name, fileInfo.IsDirectory); + var fileName = fileInfo == null ? Path.GetFileName(fullPath) : fileInfo.Name; + + item.Name = GetDisplayName(fileName, fileInfo != null && fileInfo.IsDirectory); } } @@ -170,7 +170,11 @@ namespace Emby.Server.Implementations.Library if (config.UseFileCreationTimeForDateAdded) { - item.DateCreated = fileSystem.GetCreationTimeUtc(info); + // directoryService.getFile may return null + if (info != null) + { + item.DateCreated = fileSystem.GetCreationTimeUtc(info); + } } else { diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 9a5d6b105..fa4f026f4 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -6,6 +6,7 @@ using System; using System.IO; using System.Linq; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; namespace Emby.Server.Implementations.Library.Resolvers @@ -18,9 +19,11 @@ namespace Emby.Server.Implementations.Library.Resolvers where T : Video, new() { protected readonly ILibraryManager LibraryManager; + protected readonly IFileSystem FileSystem; - protected BaseVideoResolver(ILibraryManager libraryManager) + protected BaseVideoResolver(ILibraryManager libraryManager, IFileSystem fileSystem) { + FileSystem = fileSystem; LibraryManager = libraryManager; } @@ -178,11 +181,6 @@ namespace Emby.Server.Implementations.Library.Resolvers { video.VideoType = VideoType.Dvd; } - else if (string.Equals(videoInfo.StubType, "hddvd", StringComparison.OrdinalIgnoreCase)) - { - video.VideoType = VideoType.HdDvd; - video.IsHD = true; - } else if (string.Equals(videoInfo.StubType, "bluray", StringComparison.OrdinalIgnoreCase)) { video.VideoType = VideoType.BluRay; @@ -276,7 +274,7 @@ namespace Emby.Server.Implementations.Library.Resolvers return false; } - return directoryService.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase)); + return FileSystem.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase)); } /// <summary> diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 69563e5a0..1e5c0beed 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -23,11 +23,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies /// </summary> public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver { - public MovieResolver(ILibraryManager libraryManager) - : base(libraryManager) - { - } - /// <summary> /// Gets the priority. /// </summary> @@ -74,7 +69,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) { - return ResolveVideos<MusicVideo>(parent, files, directoryService, false, collectionType); + return ResolveVideos<MusicVideo>(parent, files, directoryService, true, collectionType); } if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) || @@ -164,8 +159,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies IsInMixedFolder = isInMixedFolder, ProductionYear = video.Year, Name = video.Name, - AdditionalParts = video.Files.Skip(1).Select(i => i.Path).ToList(), - LocalAlternateVersions = video.AlternateVersions.Select(i => i.Path).ToList() + AdditionalParts = video.Files.Skip(1).Select(i => i.Path).ToArray(), + LocalAlternateVersions = video.AlternateVersions.Select(i => i.Path).ToArray() }; SetVideoType(videoItem, firstVideo); @@ -452,8 +447,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies var folderPaths = multiDiscFolders.Select(i => i.FullName).Where(i => { - var subFileEntries = directoryService.GetFileSystemEntries(i) - .ToList(); + var subFileEntries = directoryService.GetFileSystemEntries(i); var subfolders = subFileEntries .Where(e => e.IsDirectory) @@ -509,7 +503,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies { Path = folderPaths[0], - AdditionalParts = folderPaths.Skip(1).ToList(), + AdditionalParts = folderPaths.Skip(1).ToArray(), VideoType = videoTypes[0], @@ -547,5 +541,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return !validCollectionTypes.Contains(collectionType, StringComparer.OrdinalIgnoreCase); } + + public MovieResolver(ILibraryManager libraryManager, IFileSystem fileSystem) : base(libraryManager, fileSystem) + { + } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index 8bbda3b62..04312f277 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Library.Resolvers var filename = Path.GetFileNameWithoutExtension(args.Path); // Make sure the image doesn't belong to a video file - if (args.DirectoryService.GetFilePaths(_fileSystem.GetDirectoryName(args.Path)).Any(i => IsOwnedByMedia(args.GetLibraryOptions(), i, filename))) + if (_fileSystem.GetFilePaths(_fileSystem.GetDirectoryName(args.Path)).Any(i => IsOwnedByMedia(args.GetLibraryOptions(), i, filename))) { return null; } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index bdab8552a..7d1c4d65a 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -3,6 +3,7 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using System.Linq; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; namespace Emby.Server.Implementations.Library.Resolvers.TV { @@ -11,10 +12,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// </summary> public class EpisodeResolver : BaseVideoResolver<Episode> { - public EpisodeResolver(ILibraryManager libraryManager) : base(libraryManager) - { - } - /// <summary> /// Resolves the specified args. /// </summary> @@ -76,5 +73,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV return null; } + + public EpisodeResolver(ILibraryManager libraryManager, IFileSystem fileSystem) : base(libraryManager, fileSystem) + { + } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs index b5e1bf5f7..5c7a528f5 100644 --- a/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs @@ -1,6 +1,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; +using MediaBrowser.Model.IO; namespace Emby.Server.Implementations.Library.Resolvers { @@ -9,11 +10,6 @@ namespace Emby.Server.Implementations.Library.Resolvers /// </summary> public class VideoResolver : BaseVideoResolver<Video> { - public VideoResolver(ILibraryManager libraryManager) - : base(libraryManager) - { - } - protected override Video Resolve(ItemResolveArgs args) { if (args.Parent != null) @@ -33,12 +29,16 @@ namespace Emby.Server.Implementations.Library.Resolvers { get { return ResolverPriority.Last; } } + + public VideoResolver(ILibraryManager libraryManager, IFileSystem fileSystem) : base(libraryManager, fileSystem) + { + } } public class GenericVideoResolver<T> : BaseVideoResolver<T> where T : Video, new () { - public GenericVideoResolver(ILibraryManager libraryManager) : base(libraryManager) + public GenericVideoResolver(ILibraryManager libraryManager, IFileSystem fileSystem) : base(libraryManager, fileSystem) { } } diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 6f63322c8..658558ec0 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading.Tasks; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.Library { @@ -163,8 +164,8 @@ namespace Emby.Server.Implementations.Library var mediaItems = _libraryManager.GetItemList(new InternalItemsQuery(user) { NameContains = searchTerm, - ExcludeItemTypes = excludeItemTypes.ToArray(), - IncludeItemTypes = includeItemTypes.ToArray(), + ExcludeItemTypes = excludeItemTypes.ToArray(excludeItemTypes.Count), + IncludeItemTypes = includeItemTypes.ToArray(includeItemTypes.Count), Limit = query.Limit, IncludeItemsByName = string.IsNullOrWhiteSpace(query.ParentId), ParentId = string.IsNullOrWhiteSpace(query.ParentId) ? (Guid?)null : new Guid(query.ParentId), diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 8e8f8c4dc..019b8162a 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -122,7 +122,7 @@ namespace Emby.Server.Implementations.Library /// <param name="user">The user.</param> private void OnUserDeleted(User user) { - EventHelper.QueueEventIfNotNull(UserDeleted, this, new GenericEventArgs<User> { Argument = user }, _logger); + EventHelper.FireEventIfNotNull(UserDeleted, this, new GenericEventArgs<User> { Argument = user }, _logger); } #endregion diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index a6ed84f29..0d4303b16 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -15,6 +15,7 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.Library { @@ -231,7 +232,7 @@ namespace Emby.Server.Implementations.Library return list; } - private IEnumerable<BaseItem> GetItemsForLatestItems(User user, LatestItemsQuery request, DtoOptions options) + private List<BaseItem> GetItemsForLatestItems(User user, LatestItemsQuery request, DtoOptions options) { var parentId = request.ParentId; @@ -269,7 +270,41 @@ namespace Emby.Server.Implementations.Library return new List<BaseItem>(); } - var excludeItemTypes = includeItemTypes.Length == 0 ? new[] + var mediaTypes = new List<string>(); + + if (includeItemTypes.Length == 0) + { + foreach (var parent in parents.OfType<ICollectionFolder>()) + { + switch (parent.CollectionType) + { + case CollectionType.Books: + mediaTypes.Add(MediaType.Book); + break; + case CollectionType.Games: + mediaTypes.Add(MediaType.Game); + break; + case CollectionType.Music: + mediaTypes.Add(MediaType.Audio); + break; + case CollectionType.Photos: + mediaTypes.Add(MediaType.Photo); + mediaTypes.Add(MediaType.Video); + break; + case CollectionType.HomeVideos: + mediaTypes.Add(MediaType.Photo); + mediaTypes.Add(MediaType.Video); + break; + default: + mediaTypes.Add(MediaType.Video); + break; + } + } + + mediaTypes = mediaTypes.Distinct().ToList(); + } + + var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0 ? new[] { typeof(Person).Name, typeof(Studio).Name, @@ -290,7 +325,8 @@ namespace Emby.Server.Implementations.Library IsVirtualItem = false, Limit = limit * 5, IsPlayed = isPlayed, - DtoOptions = options + DtoOptions = options, + MediaTypes = mediaTypes.ToArray(mediaTypes.Count) }; if (parents.Count == 0) diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs index c2eb30ee4..39630cf96 100644 --- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -55,28 +55,21 @@ namespace Emby.Server.Implementations.Library.Validators /// <returns>Task.</returns> public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress) { - var people = _libraryManager.GetPeople(new InternalPeopleQuery()); - - var dict = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase); - - foreach (var person in people) - { - dict[person.Name] = true; - } + var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery()); var numComplete = 0; - _logger.Debug("Will refresh {0} people", dict.Count); + var numPeople = people.Count; - var numPeople = dict.Count; + _logger.Debug("Will refresh {0} people", numPeople); - foreach (var person in dict) + foreach (var person in people) { cancellationToken.ThrowIfCancellationRequested(); try { - var item = _libraryManager.GetPerson(person.Key); + var item = _libraryManager.GetPerson(person); var options = new MetadataRefreshOptions(_fileSystem) { diff --git a/Emby.Server.Implementations/LiveTv/ChannelImageProvider.cs b/Emby.Server.Implementations/LiveTv/ChannelImageProvider.cs index 95cefd999..0c8049d8b 100644 --- a/Emby.Server.Implementations/LiveTv/ChannelImageProvider.cs +++ b/Emby.Server.Implementations/LiveTv/ChannelImageProvider.cs @@ -28,12 +28,12 @@ namespace Emby.Server.Implementations.LiveTv _appHost = appHost; } - public IEnumerable<ImageType> GetSupportedImages(IHasImages item) + public IEnumerable<ImageType> GetSupportedImages(IHasMetadata item) { return new[] { ImageType.Primary }; } - public async Task<DynamicImageResponse> GetImage(IHasImages item, ImageType type, CancellationToken cancellationToken) + public async Task<DynamicImageResponse> GetImage(IHasMetadata item, ImageType type, CancellationToken cancellationToken) { var liveTvItem = (LiveTvChannel)item; @@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.LiveTv get { return "Live TV Service Provider"; } } - public bool Supports(IHasImages item) + public bool Supports(IHasMetadata item) { return item is LiveTvChannel; } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index b55e4412b..99b5558a2 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -4,7 +4,6 @@ using MediaBrowser.Common.Net; using MediaBrowser.Common.Security; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.FileOrganization; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; @@ -36,7 +35,6 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.IO; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Diagnostics; -using MediaBrowser.Model.FileOrganization; using MediaBrowser.Model.System; using MediaBrowser.Model.Threading; using MediaBrowser.Model.Extensions; @@ -61,7 +59,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly ILibraryMonitor _libraryMonitor; private readonly ILibraryManager _libraryManager; private readonly IProviderManager _providerManager; - private readonly IFileOrganizationService _organizationService; private readonly IMediaEncoder _mediaEncoder; private readonly IProcessFactory _processFactory; private readonly ISystemEvents _systemEvents; @@ -74,7 +71,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings = new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase); - public EmbyTV(IServerApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IFileOrganizationService organizationService, IMediaEncoder mediaEncoder, ITimerFactory timerFactory, IProcessFactory processFactory, ISystemEvents systemEvents) + public EmbyTV(IServerApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IMediaEncoder mediaEncoder, ITimerFactory timerFactory, IProcessFactory processFactory, ISystemEvents systemEvents) { Current = this; @@ -86,7 +83,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _libraryManager = libraryManager; _libraryMonitor = libraryMonitor; _providerManager = providerManager; - _organizationService = organizationService; _mediaEncoder = mediaEncoder; _processFactory = processFactory; _systemEvents = systemEvents; @@ -1636,7 +1632,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return; } - var episodesToDelete = (librarySeries.GetItems(new InternalItemsQuery + var episodesToDelete = (librarySeries.GetItemList(new InternalItemsQuery { SortBy = new[] { ItemSortBy.DateCreated }, SortOrder = SortOrder.Descending, @@ -1646,7 +1642,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV DtoOptions = new DtoOptions(true) })) - .Items .Where(i => i.LocationType == LocationType.FileSystem && _fileSystem.FileExists(i.Path)) .Skip(seriesTimer.KeepUpTo - 1) .ToList(); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 6173068cc..48eba4117 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -70,7 +70,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return "ts"; } - return "mp4"; + return "mkv"; } } @@ -207,9 +207,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV inputModifier += " -fflags " + string.Join("", flags.ToArray()); } - if (!string.IsNullOrWhiteSpace(GetEncodingOptions().HardwareAccelerationType)) + var videoStream = mediaSource.VideoStream; + var videoDecoder = videoStream == null ? null : new EncodingHelper(_mediaEncoder, _fileSystem, null).GetVideoDecoder(VideoType.VideoFile, videoStream, GetEncodingOptions()); + + if (!string.IsNullOrWhiteSpace(videoDecoder)) { - inputModifier += " -hwaccel auto"; + inputModifier += " " + videoDecoder; } if (mediaSource.ReadAtNativeFramerate) diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs index 5c5072192..619d2378d 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.LiveTv { try { - dto.ParentBackdropImageTags = new List<string> + dto.ParentBackdropImageTags = new string[] { _imageProcessor.GetImageCacheTag(librarySeries, image) }; @@ -218,14 +218,14 @@ namespace Emby.Server.Implementations.LiveTv } } - if (dto.ParentBackdropImageTags == null || dto.ParentBackdropImageTags.Count == 0) + if (dto.ParentBackdropImageTags == null || dto.ParentBackdropImageTags.Length == 0) { image = program.GetImageInfo(ImageType.Backdrop, 0); if (image != null) { try { - dto.ParentBackdropImageTags = new List<string> + dto.ParentBackdropImageTags = new string[] { _imageProcessor.GetImageCacheTag(program, image) }; @@ -406,7 +406,7 @@ namespace Emby.Server.Implementations.LiveTv return dto; } - internal string GetImageTag(IHasImages info) + internal string GetImageTag(IHasMetadata info) { try { diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index bf11b7d3a..3fbbc8390 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -173,7 +173,7 @@ namespace Emby.Server.Implementations.LiveTv } } - public async Task<QueryResult<LiveTvChannel>> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken) + public async Task<QueryResult<BaseItem>> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken) { var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId); @@ -208,15 +208,7 @@ namespace Emby.Server.Implementations.LiveTv internalQuery.OrderBy.Add(new Tuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending)); } - var channelResult = _libraryManager.GetItemsResult(internalQuery); - - var result = new QueryResult<LiveTvChannel> - { - Items = channelResult.Items.Cast<LiveTvChannel>().ToArray(), - TotalRecordCount = channelResult.TotalRecordCount - }; - - return result; + return _libraryManager.GetItemsResult(internalQuery); } public LiveTvChannel GetInternalChannel(string id) @@ -993,7 +985,9 @@ namespace Emby.Server.Implementations.LiveTv var queryResult = _libraryManager.QueryItems(internalQuery); - var returnArray = (await _dtoService.GetBaseItemDtos(queryResult.Items, options, user).ConfigureAwait(false)).ToArray(); + var returnList = (await _dtoService.GetBaseItemDtos(queryResult.Items, options, user) + .ConfigureAwait(false)); + var returnArray = returnList.ToArray(returnList.Count); var result = new QueryResult<BaseItemDto> { @@ -1077,7 +1071,9 @@ namespace Emby.Server.Implementations.LiveTv var user = _userManager.GetUserById(query.UserId); - var returnArray = (await _dtoService.GetBaseItemDtos(internalResult.Items, options, user).ConfigureAwait(false)).ToArray(); + var returnList = (await _dtoService.GetBaseItemDtos(internalResult.Items, options, user) + .ConfigureAwait(false)); + var returnArray = returnList.ToArray(returnList.Count); var result = new QueryResult<BaseItemDto> { @@ -1639,7 +1635,7 @@ namespace Emby.Server.Implementations.LiveTv { MediaTypes = new[] { MediaType.Video }, Recursive = true, - AncestorIds = folderIds.Select(i => i.ToString("N")).ToArray(), + AncestorIds = folderIds.Select(i => i.ToString("N")).ToArray(folderIds.Count), IsFolder = false, IsVirtualItem = false, Limit = query.Limit, @@ -1647,9 +1643,9 @@ namespace Emby.Server.Implementations.LiveTv SortBy = new[] { ItemSortBy.DateCreated }, SortOrder = SortOrder.Descending, EnableTotalRecordCount = query.EnableTotalRecordCount, - IncludeItemTypes = includeItemTypes.ToArray(), - ExcludeItemTypes = excludeItemTypes.ToArray(), - Genres = genres.ToArray(), + IncludeItemTypes = includeItemTypes.ToArray(includeItemTypes.Count), + ExcludeItemTypes = excludeItemTypes.ToArray(excludeItemTypes.Count), + Genres = genres.ToArray(genres.Count), DtoOptions = dtoOptions }); } @@ -1695,17 +1691,20 @@ namespace Emby.Server.Implementations.LiveTv var internalResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { Recursive = true, - AncestorIds = folders.Select(i => i.Id.ToString("N")).ToArray(), + AncestorIds = folders.Select(i => i.Id.ToString("N")).ToArray(folders.Count), Limit = query.Limit, SortBy = new[] { ItemSortBy.DateCreated }, SortOrder = SortOrder.Descending, EnableTotalRecordCount = query.EnableTotalRecordCount, - IncludeItemTypes = includeItemTypes.ToArray(), - ExcludeItemTypes = excludeItemTypes.ToArray(), + IncludeItemTypes = includeItemTypes.ToArray(includeItemTypes.Count), + ExcludeItemTypes = excludeItemTypes.ToArray(excludeItemTypes.Count), DtoOptions = options }); - var returnArray = (await _dtoService.GetBaseItemDtos(internalResult.Items, options, user).ConfigureAwait(false)).ToArray(); + var returnList = (await _dtoService.GetBaseItemDtos(internalResult.Items, options, user) + .ConfigureAwait(false)); + + var returnArray = returnList.ToArray(returnList.Count); return new QueryResult<BaseItemDto> { @@ -1845,6 +1844,9 @@ namespace Emby.Server.Implementations.LiveTv public async Task AddInfoToProgramDto(List<Tuple<BaseItem, BaseItemDto>> tuples, List<ItemFields> fields, User user = null) { var programTuples = new List<Tuple<BaseItemDto, string, string, string>>(); + var hasChannelImage = fields.Contains(ItemFields.ChannelImage); + var hasChannelInfo = fields.Contains(ItemFields.ChannelInfo); + var hasServiceName = fields.Contains(ItemFields.ServiceName); foreach (var tuple in tuples) { @@ -1887,7 +1889,7 @@ namespace Emby.Server.Implementations.LiveTv dto.IsPremiere = program.IsPremiere; } - if (fields.Contains(ItemFields.ChannelInfo)) + if (hasChannelInfo || hasChannelImage) { var channel = GetInternalChannel(program.ChannelId); @@ -1897,7 +1899,7 @@ namespace Emby.Server.Implementations.LiveTv dto.MediaType = channel.MediaType; dto.ChannelNumber = channel.Number; - if (channel.HasImage(ImageType.Primary)) + if (hasChannelImage && channel.HasImage(ImageType.Primary)) { dto.ChannelPrimaryImageTag = _tvDtoService.GetImageTag(channel); } @@ -1906,7 +1908,7 @@ namespace Emby.Server.Implementations.LiveTv var serviceName = program.ServiceName; - if (fields.Contains(ItemFields.ServiceName)) + if (hasServiceName) { dto.ServiceName = serviceName; } @@ -1954,7 +1956,7 @@ namespace Emby.Server.Implementations.LiveTv if (dto.MediaSources == null) { - dto.MediaSources = recording.GetMediaSources(true).ToList(); + dto.MediaSources = recording.GetMediaSources(true); } if (dto.MediaStreams == null) @@ -1993,7 +1995,9 @@ namespace Emby.Server.Implementations.LiveTv var internalResult = await GetInternalRecordings(query, options, cancellationToken).ConfigureAwait(false); - var returnArray = (await _dtoService.GetBaseItemDtos(internalResult.Items, options, user).ConfigureAwait(false)).ToArray(); + var returnList = (await _dtoService.GetBaseItemDtos(internalResult.Items, options, user) + .ConfigureAwait(false)); + var returnArray = returnList.ToArray(returnList.Count); return new QueryResult<BaseItemDto> { @@ -2081,7 +2085,7 @@ namespace Emby.Server.Implementations.LiveTv var returnArray = returnList .OrderBy(i => i.StartDate) - .ToArray(); + .ToArray(returnList.Count); return new QueryResult<TimerInfoDto> { @@ -2154,7 +2158,7 @@ namespace Emby.Server.Implementations.LiveTv await service.CancelTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false); _lastRecordingRefreshTime = DateTime.MinValue; - EventHelper.QueueEventIfNotNull(TimerCancelled, this, new GenericEventArgs<TimerEventInfo> + EventHelper.FireEventIfNotNull(TimerCancelled, this, new GenericEventArgs<TimerEventInfo> { Argument = new TimerEventInfo { @@ -2177,7 +2181,7 @@ namespace Emby.Server.Implementations.LiveTv await service.CancelSeriesTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false); _lastRecordingRefreshTime = DateTime.MinValue; - EventHelper.QueueEventIfNotNull(SeriesTimerCancelled, this, new GenericEventArgs<TimerEventInfo> + EventHelper.FireEventIfNotNull(SeriesTimerCancelled, this, new GenericEventArgs<TimerEventInfo> { Argument = new TimerEventInfo { @@ -2338,7 +2342,7 @@ namespace Emby.Server.Implementations.LiveTv TopParentIds = new[] { GetInternalLiveTvFolder(CancellationToken.None).Result.Id.ToString("N") }, DtoOptions = options - }).ToList() : new List<BaseItem>(); + }) : new List<BaseItem>(); RemoveFields(options); @@ -2347,6 +2351,7 @@ namespace Emby.Server.Implementations.LiveTv var addCurrentProgram = options.AddCurrentProgram; var addMediaSources = options.Fields.Contains(ItemFields.MediaSources); + var addServiceName = options.Fields.Contains(ItemFields.ServiceName); foreach (var tuple in tuples) { @@ -2356,13 +2361,17 @@ namespace Emby.Server.Implementations.LiveTv dto.Number = channel.Number; dto.ChannelNumber = channel.Number; dto.ChannelType = channel.ChannelType; - dto.ServiceName = channel.ServiceName; + + if (addServiceName) + { + dto.ServiceName = channel.ServiceName; + } currentChannelsDict[dto.Id] = dto; if (addMediaSources) { - dto.MediaSources = channel.GetMediaSources(true).ToList(); + dto.MediaSources = channel.GetMediaSources(true); } if (addCurrentProgram) @@ -2516,7 +2525,7 @@ namespace Emby.Server.Implementations.LiveTv _lastRecordingRefreshTime = DateTime.MinValue; _logger.Info("New recording scheduled"); - EventHelper.QueueEventIfNotNull(TimerCreated, this, new GenericEventArgs<TimerEventInfo> + EventHelper.FireEventIfNotNull(TimerCreated, this, new GenericEventArgs<TimerEventInfo> { Argument = new TimerEventInfo { @@ -2558,7 +2567,7 @@ namespace Emby.Server.Implementations.LiveTv _lastRecordingRefreshTime = DateTime.MinValue; - EventHelper.QueueEventIfNotNull(SeriesTimerCreated, this, new GenericEventArgs<TimerEventInfo> + EventHelper.FireEventIfNotNull(SeriesTimerCreated, this, new GenericEventArgs<TimerEventInfo> { Argument = new TimerEventInfo { @@ -2697,7 +2706,7 @@ namespace Emby.Server.Implementations.LiveTv return new QueryResult<BaseItemDto> { - Items = groups.ToArray(), + Items = groups.ToArray(groups.Count), TotalRecordCount = groups.Count }; } @@ -2984,7 +2993,7 @@ namespace Emby.Server.Implementations.LiveTv Name = tunerChannelId, Value = providerChannelId }); - listingsProviderInfo.ChannelMappings = list.ToArray(); + listingsProviderInfo.ChannelMappings = list.ToArray(list.Count); } _config.SaveConfiguration("livetv", config); diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index 919c0f10d..5436a12b8 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -14,6 +14,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.LiveTv { @@ -78,8 +79,7 @@ namespace Emby.Server.Implementations.LiveTv { var hasMediaSources = (IHasMediaSources)item; - sources = _mediaSourceManager.GetStaticMediaSources(hasMediaSources, false) - .ToList(); + sources = _mediaSourceManager.GetStaticMediaSources(hasMediaSources, false); forceRequireOpening = true; } @@ -103,7 +103,7 @@ namespace Emby.Server.Implementations.LiveTv openKeys.Add(item.GetType().Name); openKeys.Add(item.Id.ToString("N")); openKeys.Add(source.Id ?? string.Empty); - source.OpenToken = string.Join(StreamIdDelimeterString, openKeys.ToArray()); + source.OpenToken = string.Join(StreamIdDelimeterString, openKeys.ToArray(openKeys.Count)); } // Dummy this up so that direct play checks can still run diff --git a/Emby.Server.Implementations/LiveTv/RecordingImageProvider.cs b/Emby.Server.Implementations/LiveTv/RecordingImageProvider.cs index 47663bdbc..992badbb5 100644 --- a/Emby.Server.Implementations/LiveTv/RecordingImageProvider.cs +++ b/Emby.Server.Implementations/LiveTv/RecordingImageProvider.cs @@ -19,12 +19,12 @@ namespace Emby.Server.Implementations.LiveTv _liveTvManager = liveTvManager; } - public IEnumerable<ImageType> GetSupportedImages(IHasImages item) + public IEnumerable<ImageType> GetSupportedImages(IHasMetadata item) { return new[] { ImageType.Primary }; } - public async Task<DynamicImageResponse> GetImage(IHasImages item, ImageType type, CancellationToken cancellationToken) + public async Task<DynamicImageResponse> GetImage(IHasMetadata item, ImageType type, CancellationToken cancellationToken) { var liveTvItem = (ILiveTvRecording)item; @@ -58,7 +58,7 @@ namespace Emby.Server.Implementations.LiveTv get { return "Live TV Service Provider"; } } - public bool Supports(IHasImages item) + public bool Supports(IHasMetadata item) { return item is ILiveTvRecording; } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 113cb33f4..2c12f4ca1 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -1,5 +1,4 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -11,12 +10,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; - using MediaBrowser.Model.IO; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.IO; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.System; @@ -46,9 +43,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts get { return "M3U Tuner"; } } + private string GetFullChannelIdPrefix(TunerHostInfo info) + { + return ChannelIdPrefix + info.Url.GetMD5().ToString("N"); + } + protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken) { - var result = await new M3uParser(Logger, FileSystem, _httpClient, _appHost).Parse(info.Url, ChannelIdPrefix, info.Id, !info.EnableTvgId, cancellationToken).ConfigureAwait(false); + var channelIdPrefix = GetFullChannelIdPrefix(info); + + var result = await new M3uParser(Logger, FileSystem, _httpClient, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false); return result.Cast<ChannelInfo>().ToList(); } @@ -87,9 +91,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken) { - var urlHash = info.Url.GetMD5().ToString("N"); - var prefix = ChannelIdPrefix + urlHash; - if (!channelId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + var channelIdPrefix = GetFullChannelIdPrefix(info); + + if (!channelId.StartsWith(channelIdPrefix, StringComparison.OrdinalIgnoreCase)) { return null; } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index 8d73c7e2b..113e691b6 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -32,25 +32,21 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts _appHost = appHost; } - public async Task<List<M3UChannel>> Parse(string url, string channelIdPrefix, string tunerHostId, bool enableStreamUrlAsIdentifier, CancellationToken cancellationToken) + public async Task<List<M3UChannel>> Parse(string url, string channelIdPrefix, string tunerHostId, CancellationToken cancellationToken) { - var urlHash = url.GetMD5().ToString("N"); - // Read the file and display it line by line. using (var reader = new StreamReader(await GetListingsStream(url, cancellationToken).ConfigureAwait(false))) { - return GetChannels(reader, urlHash, channelIdPrefix, tunerHostId, enableStreamUrlAsIdentifier); + return GetChannels(reader, channelIdPrefix, tunerHostId); } } public List<M3UChannel> ParseString(string text, string channelIdPrefix, string tunerHostId) { - var urlHash = "text".GetMD5().ToString("N"); - // Read the file and display it line by line. using (var reader = new StringReader(text)) { - return GetChannels(reader, urlHash, channelIdPrefix, tunerHostId, false); + return GetChannels(reader, channelIdPrefix, tunerHostId); } } @@ -70,7 +66,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts } const string ExtInfPrefix = "#EXTINF:"; - private List<M3UChannel> GetChannels(TextReader reader, string urlHash, string channelIdPrefix, string tunerHostId, bool enableStreamUrlAsIdentifier) + private List<M3UChannel> GetChannels(TextReader reader, string channelIdPrefix, string tunerHostId) { var channels = new List<M3UChannel>(); string line; @@ -97,13 +93,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts else if (!string.IsNullOrWhiteSpace(extInf) && !line.StartsWith("#", StringComparison.OrdinalIgnoreCase)) { var channel = GetChannelnfo(extInf, tunerHostId, line); - if (string.IsNullOrWhiteSpace(channel.Id) || enableStreamUrlAsIdentifier) + if (string.IsNullOrWhiteSpace(channel.Id)) { - channel.Id = channelIdPrefix + urlHash + line.GetMD5().ToString("N"); + channel.Id = channelIdPrefix + line.GetMD5().ToString("N"); } else { - channel.Id = channelIdPrefix + urlHash + channel.Id.GetMD5().ToString("N"); + channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N"); } channel.Path = line; diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index bc0dc236d..133644ff7 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -69,7 +69,7 @@ "ViewTypeTvResume": "Resume", "ViewTypeTvNextUp": "Next Up", "ViewTypeTvLatest": "Latest", - "ViewTypeTvShowSeries": "Series", + "ViewTypeTvShowSeries": "Shows", "ViewTypeTvGenres": "Genres", "ViewTypeTvFavoriteSeries": "Favorite Series", "ViewTypeTvFavoriteEpisodes": "Favorite Episodes", diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 64a41acef..8d3051a89 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.Localization /// Gets the cultures. /// </summary> /// <returns>IEnumerable{CultureDto}.</returns> - public IEnumerable<CultureDto> GetCultures() + public List<CultureDto> GetCultures() { var type = GetType(); var path = type.Namespace + ".iso6392.txt"; @@ -169,14 +169,14 @@ namespace Emby.Server.Implementations.Localization return list.Where(i => !string.IsNullOrWhiteSpace(i.Name) && !string.IsNullOrWhiteSpace(i.DisplayName) && !string.IsNullOrWhiteSpace(i.ThreeLetterISOLanguageName) && - !string.IsNullOrWhiteSpace(i.TwoLetterISOLanguageName)); + !string.IsNullOrWhiteSpace(i.TwoLetterISOLanguageName)).ToList(); } /// <summary> /// Gets the countries. /// </summary> /// <returns>IEnumerable{CountryInfo}.</returns> - public IEnumerable<CountryInfo> GetCountries() + public List<CountryInfo> GetCountries() { var type = GetType(); var path = type.Namespace + ".countries.json"; diff --git a/Emby.Server.Implementations/Localization/Ratings/es.txt b/Emby.Server.Implementations/Localization/Ratings/es.txt index 1ba24fb99..32a1e6438 100644 --- a/Emby.Server.Implementations/Localization/Ratings/es.txt +++ b/Emby.Server.Implementations/Localization/Ratings/es.txt @@ -1,4 +1,5 @@ ES-A,1 +ES-APTA,1 ES-7,3 ES-12,6 ES-16,8 diff --git a/Emby.Server.Implementations/Localization/Ratings/ro.txt b/Emby.Server.Implementations/Localization/Ratings/ro.txt new file mode 100644 index 000000000..3fdaed9cc --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ro.txt @@ -0,0 +1 @@ +RO-AG,1
\ No newline at end of file diff --git a/Emby.Server.Implementations/Localization/Ratings/us.txt b/Emby.Server.Implementations/Localization/Ratings/us.txt index 3f5311e0e..9bd78c72b 100644 --- a/Emby.Server.Implementations/Localization/Ratings/us.txt +++ b/Emby.Server.Implementations/Localization/Ratings/us.txt @@ -1,3 +1,4 @@ +APPROVED,1 G,1 E,1 EC,1 diff --git a/Emby.Server.Implementations/Localization/TextLocalizer.cs b/Emby.Server.Implementations/Localization/TextLocalizer.cs new file mode 100644 index 000000000..5188a959e --- /dev/null +++ b/Emby.Server.Implementations/Localization/TextLocalizer.cs @@ -0,0 +1,63 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace Emby.Server.Implementations.Localization +{ + public class TextLocalizer : ITextLocalizer + { + public string RemoveDiacritics(string text) + { + if (text == null) + { + throw new ArgumentNullException("text"); + } + + var chars = Normalize(text, NormalizationForm.FormD) + .Where(ch => CharUnicodeInfo.GetUnicodeCategory(ch) != UnicodeCategory.NonSpacingMark); + + return Normalize(String.Concat(chars), NormalizationForm.FormC); + } + + private static string Normalize(string text, NormalizationForm form, bool stripStringOnFailure = true) + { + if (stripStringOnFailure) + { + try + { + return text.Normalize(form); + } + catch (ArgumentException) + { + // will throw if input contains invalid unicode chars + // https://mnaoumov.wordpress.com/2014/06/14/stripping-invalid-characters-from-utf-16-strings/ + text = StripInvalidUnicodeCharacters(text); + return Normalize(text, form, false); + } + } + + try + { + return text.Normalize(form); + } + catch (ArgumentException) + { + // if it still fails, return the original text + return text; + } + } + + private static string StripInvalidUnicodeCharacters(string str) + { + var invalidCharactersRegex = new Regex("([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])"); + return invalidCharactersRegex.Replace(str, ""); + } + + public string NormalizeFormKD(string text) + { + return text.Normalize(NormalizationForm.FormKD); + } + } +} diff --git a/Emby.Server.Implementations/Logging/ConsoleLogger.cs b/Emby.Server.Implementations/Logging/ConsoleLogger.cs new file mode 100644 index 000000000..51199f08a --- /dev/null +++ b/Emby.Server.Implementations/Logging/ConsoleLogger.cs @@ -0,0 +1,13 @@ +using System; +using MediaBrowser.Model.Logging; + +namespace Emby.Server.Implementations.Logging +{ + public class ConsoleLogger : IConsoleLogger + { + public void WriteLine(string message) + { + Console.WriteLine(message); + } + } +} diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 1d74e8788..7ee6e9e38 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -29,9 +29,9 @@ namespace Emby.Server.Implementations.MediaEncoder private readonly IChapterManager _chapterManager; private readonly ILibraryManager _libraryManager; - public EncodingManager(IFileSystem fileSystem, - ILogger logger, - IMediaEncoder encoder, + public EncodingManager(IFileSystem fileSystem, + ILogger logger, + IMediaEncoder encoder, IChapterManager chapterManager, ILibraryManager libraryManager) { _fileSystem = fileSystem; @@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.MediaEncoder /// Gets the chapter images data path. /// </summary> /// <value>The chapter images data path.</value> - private string GetChapterImagesPath(IHasImages item) + private string GetChapterImagesPath(IHasMetadata item) { return Path.Combine(item.GetInternalMetadataPath(), "chapters"); } @@ -84,13 +84,8 @@ namespace Emby.Server.Implementations.MediaEncoder /// </summary> private static readonly long FirstChapterTicks = TimeSpan.FromSeconds(15).Ticks; - public async Task<bool> RefreshChapterImages(ChapterImageRefreshOptions options, CancellationToken cancellationToken) + public async Task<bool> RefreshChapterImages(Video video, List<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken) { - var extractImages = options.ExtractImages; - var video = options.Video; - var chapters = options.Chapters; - var saveChapters = options.SaveChapters; - if (!IsEligibleForChapterImageExtraction(video)) { extractImages = false; @@ -117,16 +112,14 @@ namespace Emby.Server.Implementations.MediaEncoder { if (extractImages) { - if (video.VideoType == VideoType.HdDvd || video.VideoType == VideoType.Iso) + if (video.VideoType == VideoType.Iso) { continue; } + if (video.VideoType == VideoType.BluRay || video.VideoType == VideoType.Dvd) { - if (video.PlayableStreamFileNames.Count != 1) - { - continue; - } + continue; } try @@ -136,13 +129,13 @@ namespace Emby.Server.Implementations.MediaEncoder var protocol = MediaProtocol.File; - var inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, video.Path, protocol, null, video.PlayableStreamFileNames); + var inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, video.Path, protocol, null, new List<string>()); _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path)); var container = video.Container; - var tempFile = await _encoder.ExtractVideoImage(inputPath, container, protocol, video.Video3DFormat, time, cancellationToken).ConfigureAwait(false); + var tempFile = await _encoder.ExtractVideoImage(inputPath, container, protocol, video.GetDefaultVideoStream(), video.Video3DFormat, time, cancellationToken).ConfigureAwait(false); _fileSystem.CopyFile(tempFile, path, true); try @@ -151,7 +144,7 @@ namespace Emby.Server.Implementations.MediaEncoder } catch { - + } chapter.ImagePath = path; @@ -181,7 +174,7 @@ namespace Emby.Server.Implementations.MediaEncoder if (saveChapters && changesMade) { - await _chapterManager.SaveChapters(video.Id.ToString(), chapters, cancellationToken).ConfigureAwait(false); + await _chapterManager.SaveChapters(video.Id.ToString(), chapters).ConfigureAwait(false); } DeleteDeadImages(currentImages, chapters); diff --git a/Emby.Server.Implementations/Notifications/SqliteNotificationsRepository.cs b/Emby.Server.Implementations/Notifications/SqliteNotificationsRepository.cs index 40752db80..ff152c9e9 100644 --- a/Emby.Server.Implementations/Notifications/SqliteNotificationsRepository.cs +++ b/Emby.Server.Implementations/Notifications/SqliteNotificationsRepository.cs @@ -12,6 +12,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Notifications; using SQLitePCL.pretty; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.Notifications { @@ -83,13 +84,13 @@ namespace Emby.Server.Implementations.Notifications clauses.Add("UserId=?"); paramList.Add(query.UserId.ToGuidBlob()); - var whereClause = " where " + string.Join(" And ", clauses.ToArray()); + var whereClause = " where " + string.Join(" And ", clauses.ToArray(clauses.Count)); using (WriteLock.Read()) { using (var connection = CreateConnection(true)) { - result.TotalRecordCount = connection.Query("select count(Id) from Notifications" + whereClause, paramList.ToArray()).SelectScalarInt().First(); + result.TotalRecordCount = connection.Query("select count(Id) from Notifications" + whereClause, paramList.ToArray(paramList.Count)).SelectScalarInt().First(); var commandText = string.Format("select Id,UserId,Date,Name,Description,Url,Level,IsRead,Category,RelatedId from Notifications{0} order by IsRead asc, Date desc", whereClause); @@ -110,12 +111,12 @@ namespace Emby.Server.Implementations.Notifications var resultList = new List<Notification>(); - foreach (var row in connection.Query(commandText, paramList.ToArray())) + foreach (var row in connection.Query(commandText, paramList.ToArray(paramList.Count))) { resultList.Add(GetNotification(row)); } - result.Notifications = resultList.ToArray(); + result.Notifications = resultList.ToArray(resultList.Count); } } @@ -280,7 +281,7 @@ namespace Emby.Server.Implementations.Notifications public async Task MarkRead(IEnumerable<string> notificationIdList, string userId, bool isRead, CancellationToken cancellationToken) { var list = notificationIdList.ToList(); - var idArray = list.Select(i => new Guid(i)).ToArray(); + var idArray = list.Select(i => new Guid(i)).ToArray(list.Count); await MarkReadInternal(idArray, userId, isRead, cancellationToken).ConfigureAwait(false); @@ -290,7 +291,7 @@ namespace Emby.Server.Implementations.Notifications { NotificationsMarkedRead(this, new NotificationReadEventArgs { - IdList = list.ToArray(), + IdList = list.ToArray(list.Count), IsRead = isRead, UserId = userId }); diff --git a/Emby.Server.Implementations/Notifications/WebSocketNotifier.cs b/Emby.Server.Implementations/Notifications/WebSocketNotifier.cs index 0239ee336..0d89ba84f 100644 --- a/Emby.Server.Implementations/Notifications/WebSocketNotifier.cs +++ b/Emby.Server.Implementations/Notifications/WebSocketNotifier.cs @@ -2,6 +2,7 @@ using MediaBrowser.Controller.Notifications; using MediaBrowser.Controller.Plugins; using System.Linq; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.Notifications { @@ -33,7 +34,7 @@ namespace Emby.Server.Implementations.Notifications list.Add(e.UserId); list.Add(e.IsRead.ToString().ToLower()); - var msg = string.Join("|", list.ToArray()); + var msg = string.Join("|", list.ToArray(list.Count)); _serverManager.SendWebSocketMessage("NotificationsMarkedRead", msg); } diff --git a/Emby.Server.Implementations/Photos/PhotoAlbumImageProvider.cs b/Emby.Server.Implementations/Photos/PhotoAlbumImageProvider.cs index 17f9b491d..f7c65f63d 100644 --- a/Emby.Server.Implementations/Photos/PhotoAlbumImageProvider.cs +++ b/Emby.Server.Implementations/Photos/PhotoAlbumImageProvider.cs @@ -18,15 +18,15 @@ namespace Emby.Server.Implementations.Photos { } - protected override List<BaseItem> GetItemsWithImages(IHasImages item) + protected override List<BaseItem> GetItemsWithImages(IHasMetadata item) { var photoAlbum = (PhotoAlbum)item; - var items = GetFinalItems(photoAlbum.Children.ToList()); + var items = GetFinalItems(photoAlbum.Children); return items; } - protected override string CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + protected override string CreateImage(IHasMetadata item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); } diff --git a/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs b/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs index 127ce24ae..c2e860339 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs @@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Playlists { } - protected override List<BaseItem> GetItemsWithImages(IHasImages item) + protected override List<BaseItem> GetItemsWithImages(IHasMetadata item) { var playlist = (Playlist)item; @@ -65,8 +65,7 @@ namespace Emby.Server.Implementations.Playlists return null; }) .Where(i => i != null) - .DistinctBy(i => i.Id) - .ToList(); + .DistinctBy(i => i.Id); return GetFinalItems(items); } @@ -81,7 +80,7 @@ namespace Emby.Server.Implementations.Playlists _libraryManager = libraryManager; } - protected override List<BaseItem> GetItemsWithImages(IHasImages item) + protected override List<BaseItem> GetItemsWithImages(IHasMetadata item) { var items = _libraryManager.GetItemList(new InternalItemsQuery { @@ -93,12 +92,12 @@ namespace Emby.Server.Implementations.Playlists ImageTypes = new[] { ImageType.Primary }, DtoOptions = new DtoOptions(false) - }).ToList(); + }); return GetFinalItems(items); } - //protected override Task<string> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + //protected override Task<string> CreateImage(IHasMetadata item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) //{ // return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); //} @@ -113,7 +112,7 @@ namespace Emby.Server.Implementations.Playlists _libraryManager = libraryManager; } - protected override List<BaseItem> GetItemsWithImages(IHasImages item) + protected override List<BaseItem> GetItemsWithImages(IHasMetadata item) { var items = _libraryManager.GetItemList(new InternalItemsQuery { @@ -125,12 +124,12 @@ namespace Emby.Server.Implementations.Playlists ImageTypes = new[] { ImageType.Primary }, DtoOptions = new DtoOptions(false) - }).ToList(); + }); return GetFinalItems(items); } - //protected override Task<string> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + //protected override Task<string> CreateImage(IHasMetadata item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) //{ // return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); //} diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index e0e133e38..578a2321c 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -12,10 +12,9 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; - using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.Playlists { @@ -164,7 +163,7 @@ namespace Emby.Server.Implementations.Playlists return path; } - private IEnumerable<BaseItem> GetPlaylistItems(IEnumerable<string> itemIds, string playlistMediaType, User user, DtoOptions options) + private List<BaseItem> GetPlaylistItems(IEnumerable<string> itemIds, string playlistMediaType, User user, DtoOptions options) { var items = itemIds.Select(i => _libraryManager.GetItemById(i)).Where(i => i != null); @@ -206,7 +205,9 @@ namespace Emby.Server.Implementations.Playlists list.Add(LinkedChild.Create(item)); } - playlist.LinkedChildren.AddRange(list); + var newList = playlist.LinkedChildren.ToList(); + newList.AddRange(list); + playlist.LinkedChildren = newList.ToArray(newList.Count); await playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); @@ -234,7 +235,7 @@ namespace Emby.Server.Implementations.Playlists playlist.LinkedChildren = children.Except(removals) .Select(i => i.Item1) - .ToList(); + .ToArray(); await playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); @@ -265,17 +266,21 @@ namespace Emby.Server.Implementations.Playlists var item = playlist.LinkedChildren[oldIndex]; - playlist.LinkedChildren.Remove(item); + var newList = playlist.LinkedChildren.ToList(); - if (newIndex >= playlist.LinkedChildren.Count) + newList.Remove(item); + + if (newIndex >= newList.Count) { - playlist.LinkedChildren.Add(item); + newList.Add(item); } else { - playlist.LinkedChildren.Insert(newIndex, item); + newList.Insert(newIndex, item); } + playlist.LinkedChildren = newList.ToArray(newList.Count); + await playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); } diff --git a/Emby.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs index 967e7ddd8..ec371c741 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs @@ -15,6 +15,7 @@ using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Tasks; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.ScheduledTasks { @@ -123,16 +124,9 @@ namespace Emby.Server.Implementations.ScheduledTasks try { - var chapters = _itemRepo.GetChapters(video.Id).ToList(); + var chapters = _itemRepo.GetChapters(video.Id); - var success = await _encodingManager.RefreshChapterImages(new ChapterImageRefreshOptions - { - SaveChapters = true, - ExtractImages = extract, - Video = video, - Chapters = chapters - - }, CancellationToken.None); + var success = await _encodingManager.RefreshChapterImages(video, chapters, extract, true, CancellationToken.None); if (!success) { @@ -142,7 +136,7 @@ namespace Emby.Server.Implementations.ScheduledTasks _fileSystem.CreateDirectory(parentPath); - _fileSystem.WriteAllText(failHistoryPath, string.Join("|", previouslyFailedImages.ToArray())); + _fileSystem.WriteAllText(failHistoryPath, string.Join("|", previouslyFailedImages.ToArray(previouslyFailedImages.Count))); } numComplete++; diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs index 9ec0af6bb..d512ff4fb 100644 --- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs +++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs @@ -11,6 +11,7 @@ using MediaBrowser.Controller.Security; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Querying; using SQLitePCL.pretty; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.Security { @@ -174,13 +175,13 @@ namespace Emby.Server.Implementations.Security var whereTextWithoutPaging = whereClauses.Count == 0 ? string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); + " where " + string.Join(" AND ", whereClauses.ToArray(whereClauses.Count)); if (startIndex > 0) { var pagingWhereText = whereClauses.Count == 0 ? string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); + " where " + string.Join(" AND ", whereClauses.ToArray(whereClauses.Count)); whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM AccessTokens {0} ORDER BY DateCreated LIMIT {1})", pagingWhereText, @@ -189,7 +190,7 @@ namespace Emby.Server.Implementations.Security var whereText = whereClauses.Count == 0 ? string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); + " where " + string.Join(" AND ", whereClauses.ToArray(whereClauses.Count)); commandText += whereText; @@ -236,7 +237,7 @@ namespace Emby.Server.Implementations.Security } } - result.Items = list.ToArray(); + result.Items = list.ToArray(list.Count); return result; }, ReadTransactionMode); diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs index 1c530144c..4ad56411a 100644 --- a/Emby.Server.Implementations/Services/ServiceController.cs +++ b/Emby.Server.Implementations/Services/ServiceController.cs @@ -15,17 +15,15 @@ namespace Emby.Server.Implementations.Services public class ServiceController { public static ServiceController Instance; - private readonly Func<IEnumerable<Type>> _resolveServicesFn; - public ServiceController(Func<IEnumerable<Type>> resolveServicesFn) + public ServiceController() { Instance = this; - _resolveServicesFn = resolveServicesFn; } - public void Init(HttpListenerHost appHost) + public void Init(HttpListenerHost appHost, Type[] serviceTypes) { - foreach (var serviceType in _resolveServicesFn()) + foreach (var serviceType in serviceTypes) { RegisterService(appHost, serviceType); } diff --git a/Emby.Server.Implementations/Services/ServiceExec.cs b/Emby.Server.Implementations/Services/ServiceExec.cs index e0b5e69c0..4a2199c89 100644 --- a/Emby.Server.Implementations/Services/ServiceExec.cs +++ b/Emby.Server.Implementations/Services/ServiceExec.cs @@ -5,6 +5,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; using MediaBrowser.Model.Services; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.Services { @@ -123,7 +124,7 @@ namespace Emby.Server.Implementations.Services } if (reqFilters.Count > 0) - actionCtx.RequestFilters = reqFilters.OrderBy(i => i.Priority).ToArray(); + actionCtx.RequestFilters = reqFilters.OrderBy(i => i.Priority).ToArray(reqFilters.Count); actions.Add(actionCtx); } diff --git a/Emby.Server.Implementations/Services/ServicePath.cs b/Emby.Server.Implementations/Services/ServicePath.cs index 255b20919..da5e74f1f 100644 --- a/Emby.Server.Implementations/Services/ServicePath.cs +++ b/Emby.Server.Implementations/Services/ServicePath.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Text; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.Services { @@ -142,13 +143,13 @@ namespace Emby.Server.Implementations.Services } } - var components = componentsList.ToArray(); + var components = componentsList.ToArray(componentsList.Count); this.TotalComponentsCount = components.Length; this.literalsToMatch = new string[this.TotalComponentsCount]; this.variablesNames = new string[this.TotalComponentsCount]; this.isWildcard = new bool[this.TotalComponentsCount]; - this.componentsWithSeparators = hasSeparators.ToArray(); + this.componentsWithSeparators = hasSeparators.ToArray(hasSeparators.Count); this.PathComponentsCount = this.componentsWithSeparators.Length; string firstLiteralMatch = null; @@ -268,7 +269,7 @@ namespace Emby.Server.Implementations.Services propertyInfos.InsertRange(0, newPropertyInfos); } - return propertyInfos.ToArray(); + return propertyInfos.ToArray(propertyInfos.Count); } return GetTypesPublicProperties(type) @@ -285,7 +286,7 @@ namespace Emby.Server.Implementations.Services if (mi != null && mi.IsStatic) continue; pis.Add(pi); } - return pis.ToArray(); + return pis.ToArray(pis.Count); } @@ -450,7 +451,7 @@ namespace Emby.Server.Implementations.Services } } - withPathInfoParts = totalComponents.ToArray(); + withPathInfoParts = totalComponents.ToArray(totalComponents.Count); return true; } diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 68ab25947..9a1a229c5 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -32,6 +32,7 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Threading; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.Session { @@ -1000,7 +1001,7 @@ namespace Emby.Server.Implementations.Session command.PlayCommand = PlayCommand.PlayNow; } - command.ItemIds = items.Select(i => i.Id.ToString("N")).ToArray(); + command.ItemIds = items.Select(i => i.Id.ToString("N")).ToArray(items.Count); if (user != null) { @@ -1033,7 +1034,7 @@ namespace Emby.Server.Implementations.Session if (episodes.Count > 0) { - command.ItemIds = episodes.Select(i => i.Id.ToString("N")).ToArray(); + command.ItemIds = episodes.Select(i => i.Id.ToString("N")).ToArray(episodes.Count); } } } @@ -1089,7 +1090,7 @@ namespace Emby.Server.Implementations.Session { var folder = (Folder)item; - var itemsResult = folder.GetItems(new InternalItemsQuery(user) + var itemsResult = folder.GetItemList(new InternalItemsQuery(user) { Recursive = true, IsFolder = false, @@ -1104,7 +1105,7 @@ namespace Emby.Server.Implementations.Session }); - return FilterToSingleMediaType(itemsResult.Items) + return FilterToSingleMediaType(itemsResult) .OrderBy(i => i.SortName) .ToList(); } @@ -1675,7 +1676,6 @@ namespace Emby.Server.Implementations.Session dtoOptions.Fields.Remove(ItemFields.ExternalEtag); dtoOptions.Fields.Remove(ItemFields.InheritedParentalRatingValue); dtoOptions.Fields.Remove(ItemFields.ItemCounts); - dtoOptions.Fields.Remove(ItemFields.Keywords); dtoOptions.Fields.Remove(ItemFields.MediaSourceCount); dtoOptions.Fields.Remove(ItemFields.MediaStreams); dtoOptions.Fields.Remove(ItemFields.MediaSources); @@ -1686,7 +1686,6 @@ namespace Emby.Server.Implementations.Session dtoOptions.Fields.Remove(ItemFields.RecursiveItemCount); dtoOptions.Fields.Remove(ItemFields.RemoteTrailers); dtoOptions.Fields.Remove(ItemFields.SeasonUserData); - dtoOptions.Fields.Remove(ItemFields.SeriesGenres); dtoOptions.Fields.Remove(ItemFields.Settings); dtoOptions.Fields.Remove(ItemFields.SortName); dtoOptions.Fields.Remove(ItemFields.Tags); diff --git a/Emby.Server.Implementations/Social/SharingRepository.cs b/Emby.Server.Implementations/Social/SharingRepository.cs index 46e9205bb..a2a1f879a 100644 --- a/Emby.Server.Implementations/Social/SharingRepository.cs +++ b/Emby.Server.Implementations/Social/SharingRepository.cs @@ -8,6 +8,7 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Social; using SQLitePCL.pretty; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.Social { @@ -86,7 +87,7 @@ namespace Emby.Server.Implementations.Social var paramList = new List<object>(); paramList.Add(id.ToGuidBlob()); - foreach (var row in connection.Query(commandText, paramList.ToArray())) + foreach (var row in connection.Query(commandText, paramList.ToArray(paramList.Count))) { return GetSocialShareInfo(row); } diff --git a/Emby.Server.Implementations/Sorting/AirTimeComparer.cs b/Emby.Server.Implementations/Sorting/AirTimeComparer.cs deleted file mode 100644 index bc05e9af3..000000000 --- a/Emby.Server.Implementations/Sorting/AirTimeComparer.cs +++ /dev/null @@ -1,71 +0,0 @@ -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Sorting; -using MediaBrowser.Model.Querying; -using System; - -namespace Emby.Server.Implementations.Sorting -{ - public class AirTimeComparer : IBaseItemComparer - { - /// <summary> - /// Compares the specified x. - /// </summary> - /// <param name="x">The x.</param> - /// <param name="y">The y.</param> - /// <returns>System.Int32.</returns> - public int Compare(BaseItem x, BaseItem y) - { - return DateTime.Compare(GetValue(x), GetValue(y)); - } - - /// <summary> - /// Gets the value. - /// </summary> - /// <param name="x">The x.</param> - /// <returns>System.String.</returns> - private DateTime GetValue(BaseItem x) - { - var series = x as Series; - - if (series == null) - { - var season = x as Season; - - if (season != null) - { - series = season.Series; - } - else - { - var episode = x as Episode; - - if (episode != null) - { - series = episode.Series; - } - } - } - - if (series != null) - { - DateTime result; - if (DateTime.TryParse(series.AirTime, out result)) - { - return result; - } - } - - return DateTime.MinValue; - } - - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - public string Name - { - get { return ItemSortBy.AirTime; } - } - } -} diff --git a/Emby.Server.Implementations/SystemEvents.cs b/Emby.Server.Implementations/SystemEvents.cs new file mode 100644 index 000000000..dfff92f1e --- /dev/null +++ b/Emby.Server.Implementations/SystemEvents.cs @@ -0,0 +1,50 @@ +using System; +using MediaBrowser.Common.Events; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.System; + +namespace Emby.Server.Implementations +{ + public class SystemEvents : ISystemEvents + { + public event EventHandler Resume; + public event EventHandler Suspend; + public event EventHandler SessionLogoff; + public event EventHandler SystemShutdown; + + private readonly ILogger _logger; + + public SystemEvents(ILogger logger) + { + _logger = logger; + Microsoft.Win32.SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; + Microsoft.Win32.SystemEvents.SessionEnding += SystemEvents_SessionEnding; + } + + private void SystemEvents_SessionEnding(object sender, Microsoft.Win32.SessionEndingEventArgs e) + { + switch (e.Reason) + { + case Microsoft.Win32.SessionEndReasons.Logoff: + EventHelper.FireEventIfNotNull(SessionLogoff, this, EventArgs.Empty, _logger); + break; + case Microsoft.Win32.SessionEndReasons.SystemShutdown: + EventHelper.FireEventIfNotNull(SystemShutdown, this, EventArgs.Empty, _logger); + break; + } + } + + private void SystemEvents_PowerModeChanged(object sender, Microsoft.Win32.PowerModeChangedEventArgs e) + { + switch (e.Mode) + { + case Microsoft.Win32.PowerModes.Resume: + EventHelper.FireEventIfNotNull(Resume, this, EventArgs.Empty, _logger); + break; + case Microsoft.Win32.PowerModes.Suspend: + EventHelper.FireEventIfNotNull(Suspend, this, EventArgs.Empty, _logger); + break; + } + } + } +} diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index 876c5d58b..03283031e 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.TV int? limit = null; if (!string.IsNullOrWhiteSpace(request.SeriesId)) { - var series = _libraryManager.GetItemById(request.SeriesId); + var series = _libraryManager.GetItemById(request.SeriesId) as Series; if (series != null) { @@ -51,17 +51,22 @@ namespace Emby.Server.Implementations.TV } } - if (string.IsNullOrWhiteSpace(presentationUniqueKey) && limit.HasValue) + if (!string.IsNullOrWhiteSpace(presentationUniqueKey)) + { + return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, dtoOptions), request); + } + + if (limit.HasValue) { limit = limit.Value + 10; } var items = _libraryManager.GetItemList(new InternalItemsQuery(user) { - IncludeItemTypes = new[] { typeof(Series).Name }, - SortBy = new[] { ItemSortBy.SeriesDatePlayed }, + IncludeItemTypes = new[] { typeof(Episode).Name }, + SortBy = new[] { ItemSortBy.DatePlayed }, SortOrder = SortOrder.Descending, - PresentationUniqueKey = presentationUniqueKey, + SeriesPresentationUniqueKey = presentationUniqueKey, Limit = limit, ParentId = parentIdGuid, Recursive = true, @@ -69,11 +74,12 @@ namespace Emby.Server.Implementations.TV { Fields = new List<ItemFields> { - ItemFields.PresentationUniqueKey + ItemFields.SeriesPresentationUniqueKey } - } + }, + GroupBySeriesPresentationUniqueKey = true - }).Cast<Series>().Select(GetUniqueSeriesKey); + }).Cast<Episode>().Select(GetUniqueSeriesKey); // Avoid implicitly captured closure var episodes = GetNextUpEpisodes(request, user, items, dtoOptions); @@ -94,7 +100,7 @@ namespace Emby.Server.Implementations.TV int? limit = null; if (!string.IsNullOrWhiteSpace(request.SeriesId)) { - var series = _libraryManager.GetItemById(request.SeriesId); + var series = _libraryManager.GetItemById(request.SeriesId) as Series; if (series != null) { @@ -103,28 +109,34 @@ namespace Emby.Server.Implementations.TV } } - if (string.IsNullOrWhiteSpace(presentationUniqueKey) && limit.HasValue) + if (!string.IsNullOrWhiteSpace(presentationUniqueKey)) + { + return GetResult(GetNextUpEpisodes(request, user, new [] { presentationUniqueKey }, dtoOptions), request); + } + + if (limit.HasValue) { limit = limit.Value + 10; } var items = _libraryManager.GetItemList(new InternalItemsQuery(user) { - IncludeItemTypes = new[] { typeof(Series).Name }, - SortBy = new[] { ItemSortBy.SeriesDatePlayed }, + IncludeItemTypes = new[] { typeof(Episode).Name }, + SortBy = new[] { ItemSortBy.DatePlayed }, SortOrder = SortOrder.Descending, - PresentationUniqueKey = presentationUniqueKey, + SeriesPresentationUniqueKey = presentationUniqueKey, Limit = limit, DtoOptions = new MediaBrowser.Controller.Dto.DtoOptions { Fields = new List<ItemFields> { - ItemFields.PresentationUniqueKey + ItemFields.SeriesPresentationUniqueKey }, EnableImages = false - } + }, + GroupBySeriesPresentationUniqueKey = true - }, parentsFolders.Cast<BaseItem>().ToList()).Cast<Series>().Select(GetUniqueSeriesKey); + }, parentsFolders.Cast<BaseItem>().ToList()).Cast<Episode>().Select(GetUniqueSeriesKey); // Avoid implicitly captured closure var episodes = GetNextUpEpisodes(request, user, items, dtoOptions); @@ -167,7 +179,12 @@ namespace Emby.Server.Implementations.TV .Where(i => i != null); } - private string GetUniqueSeriesKey(BaseItem series) + private string GetUniqueSeriesKey(Episode episode) + { + return episode.SeriesPresentationUniqueKey; + } + + private string GetUniqueSeriesKey(Series series) { return series.GetPresentationUniqueKey(); } diff --git a/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs index f54613384..98e0c4080 100644 --- a/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.UserViews { } - public override IEnumerable<ImageType> GetSupportedImages(IHasImages item) + public override IEnumerable<ImageType> GetSupportedImages(IHasMetadata item) { return new List<ImageType> { @@ -36,13 +36,13 @@ namespace Emby.Server.Implementations.UserViews }; } - protected override List<BaseItem> GetItemsWithImages(IHasImages item) + protected override List<BaseItem> GetItemsWithImages(IHasMetadata item) { var view = (CollectionFolder)item; var recursive = !new[] { CollectionType.Playlists, CollectionType.Channels }.Contains(view.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); - var result = view.GetItems(new InternalItemsQuery + var result = view.GetItemList(new InternalItemsQuery { CollapseBoxSetItems = false, Recursive = recursive, @@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.UserViews }); - var items = result.Items.Select(i => + var items = result.Select(i => { var episode = i as Episode; if (episode != null) @@ -91,15 +91,15 @@ namespace Emby.Server.Implementations.UserViews }).DistinctBy(i => i.Id); - return GetFinalItems(items.Where(i => i.HasImage(ImageType.Primary) || i.HasImage(ImageType.Thumb)).ToList(), 8); + return GetFinalItems(items.Where(i => i.HasImage(ImageType.Primary) || i.HasImage(ImageType.Thumb)), 8); } - protected override bool Supports(IHasImages item) + protected override bool Supports(IHasMetadata item) { return item is CollectionFolder; } - protected override string CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + protected override string CreateImage(IHasMetadata item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { var outputPath = Path.ChangeExtension(outputPathWithoutExtension, ".png"); @@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.UserViews _libraryManager = libraryManager; } - public override IEnumerable<ImageType> GetSupportedImages(IHasImages item) + public override IEnumerable<ImageType> GetSupportedImages(IHasMetadata item) { return new List<ImageType> { @@ -134,7 +134,7 @@ namespace Emby.Server.Implementations.UserViews }; } - protected override List<BaseItem> GetItemsWithImages(IHasImages item) + protected override List<BaseItem> GetItemsWithImages(IHasMetadata item) { var view = (ManualCollectionsFolder)item; @@ -149,15 +149,15 @@ namespace Emby.Server.Implementations.UserViews DtoOptions = new DtoOptions(false) }); - return GetFinalItems(items.Where(i => i.HasImage(ImageType.Primary) || i.HasImage(ImageType.Thumb)).ToList(), 8); + return GetFinalItems(items.Where(i => i.HasImage(ImageType.Primary) || i.HasImage(ImageType.Thumb)), 8); } - protected override bool Supports(IHasImages item) + protected override bool Supports(IHasMetadata item) { return item is ManualCollectionsFolder; } - protected override string CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + protected override string CreateImage(IHasMetadata item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { var outputPath = Path.ChangeExtension(outputPathWithoutExtension, ".png"); diff --git a/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs b/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs index cd2c4728f..885dfec58 100644 --- a/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs +++ b/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs @@ -31,7 +31,7 @@ namespace Emby.Server.Implementations.UserViews _libraryManager = libraryManager; } - public override IEnumerable<ImageType> GetSupportedImages(IHasImages item) + public override IEnumerable<ImageType> GetSupportedImages(IHasMetadata item) { var view = (UserView)item; if (IsUsingCollectionStrip(view)) @@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.UserViews }; } - protected override List<BaseItem> GetItemsWithImages(IHasImages item) + protected override List<BaseItem> GetItemsWithImages(IHasMetadata item) { var view = (UserView)item; @@ -62,27 +62,27 @@ namespace Emby.Server.Implementations.UserViews IsMovie = true, DtoOptions = new DtoOptions(false) - }).ToList(); + }); - return GetFinalItems(programs).ToList(); + return GetFinalItems(programs); } if (string.Equals(view.ViewType, SpecialFolder.MovieGenre, StringComparison.OrdinalIgnoreCase) || string.Equals(view.ViewType, SpecialFolder.TvGenre, StringComparison.OrdinalIgnoreCase)) { - var userItemsResult = view.GetItems(new InternalItemsQuery + var userItemsResult = view.GetItemList(new InternalItemsQuery { CollapseBoxSetItems = false, DtoOptions = new DtoOptions(false) }); - return userItemsResult.Items.ToList(); + return userItemsResult.ToList(); } var isUsingCollectionStrip = IsUsingCollectionStrip(view); var recursive = isUsingCollectionStrip && !new[] { CollectionType.Channels, CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); - var result = view.GetItems(new InternalItemsQuery + var result = view.GetItemList(new InternalItemsQuery { User = view.UserId.HasValue ? _userManager.GetUserById(view.UserId.Value) : null, CollapseBoxSetItems = false, @@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.UserViews DtoOptions = new DtoOptions(false) }); - var items = result.Items.Select(i => + var items = result.Select(i => { var episode = i as Episode; if (episode != null) @@ -133,13 +133,13 @@ namespace Emby.Server.Implementations.UserViews if (isUsingCollectionStrip) { - return GetFinalItems(items.Where(i => i.HasImage(ImageType.Primary) || i.HasImage(ImageType.Thumb)).ToList(), 8); + return GetFinalItems(items.Where(i => i.HasImage(ImageType.Primary) || i.HasImage(ImageType.Thumb)), 8); } - return GetFinalItems(items.Where(i => i.HasImage(ImageType.Primary)).ToList()); + return GetFinalItems(items.Where(i => i.HasImage(ImageType.Primary))); } - protected override bool Supports(IHasImages item) + protected override bool Supports(IHasMetadata item) { var view = item as UserView; if (view != null) @@ -163,7 +163,7 @@ namespace Emby.Server.Implementations.UserViews return collectionStripViewTypes.Contains(view.ViewType ?? string.Empty); } - protected override string CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + protected override string CreateImage(IHasMetadata item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { if (itemsWithImages.Count == 0) { diff --git a/Emby.Server.Implementations/packages.config b/Emby.Server.Implementations/packages.config index 03336c936..3675e1950 100644 --- a/Emby.Server.Implementations/packages.config +++ b/Emby.Server.Implementations/packages.config @@ -2,6 +2,9 @@ <packages> <package id="Emby.XmlTv" version="1.0.9" targetFramework="net46" /> <package id="MediaBrowser.Naming" version="1.0.5" targetFramework="portable45-net45+win8" /> + <package id="Microsoft.IO.RecyclableMemoryStream" version="1.2.2" targetFramework="net46" /> + <package id="ServiceStack.Text" version="4.5.8" targetFramework="net46" /> + <package id="SimpleInjector" version="4.0.8" targetFramework="net46" /> <package id="SQLitePCL.pretty" version="1.1.0" targetFramework="portable45-net45+win8" /> - <package id="SQLitePCLRaw.core" version="1.1.7" targetFramework="net46" /> + <package id="SQLitePCLRaw.core" version="1.1.8" targetFramework="net46" /> </packages>
\ No newline at end of file |
