diff options
Diffstat (limited to 'Emby.Server.Implementations')
75 files changed, 11379 insertions, 321 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..f1f24660c --- /dev/null +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -0,0 +1,1749 @@ +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 ISyncRepository SyncRepository { 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(3); + var updateLevel = SystemUpdateLevel; + + if (updateLevel == PackageVersionClass.Beta) + { + cacheLength = TimeSpan.FromHours(1); + } + else if (updateLevel == PackageVersionClass.Dev) + { + 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..5e97cd5f5 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) 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/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 72c069073..a34c90cb4 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -136,24 +136,6 @@ namespace Emby.Server.Implementations.Data queries.Add("PRAGMA temp_store = file"); } - ////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)) - { - queries.Add("PRAGMA journal_mode=WAL"); - - using (WriteLock.Write()) - { - db.ExecuteAll(string.Join(";", queries.ToArray())); - } - } - else*/ foreach (var query in queries) { db.Execute(query); @@ -212,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/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 44b2cd10b..23d46e821 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 { @@ -836,7 +837,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())); } @@ -845,7 +846,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())); } @@ -865,9 +866,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 { @@ -984,16 +985,16 @@ namespace Emby.Server.Implementations.Data 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())); } @@ -1002,7 +1003,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())); } @@ -1089,9 +1090,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; } @@ -1108,22 +1109,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) @@ -1678,7 +1681,7 @@ namespace Emby.Server.Implementations.Data return parsedValue; } return (MetadataFields?)null; - }).Where(i => i.HasValue).Select(i => i.Value).ToList(); + }).Where(i => i.HasValue).Select(i => i.Value).ToArray(); } index++; } @@ -1687,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++; } @@ -1696,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++; } @@ -1873,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++; } @@ -1882,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++; } @@ -1891,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++; } @@ -1950,12 +1953,26 @@ 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) { return new List<ItemReview>(); } @@ -2206,7 +2223,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(); @@ -2548,11 +2565,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) @@ -2595,7 +2612,7 @@ namespace Emby.Server.Implementations.Data if (groups.Count > 0) { - return " Group by " + string.Join(",", groups.ToArray()); + return " Group by " + string.Join(",", groups.ToArray(groups.Count)); } return string.Empty; @@ -2632,7 +2649,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; @@ -2689,7 +2706,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; @@ -2842,7 +2859,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 }; } @@ -2865,7 +2882,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; @@ -2926,8 +2943,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) { @@ -2981,7 +2997,7 @@ namespace Emby.Server.Implementations.Data LogQueryTime("GetItems", commandText, now); - result.Items = list.ToArray(); + result.Items = list.ToArray(list.Count); return result; }, ReadTransactionMode); @@ -3133,7 +3149,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; @@ -3204,7 +3220,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; @@ -3277,7 +3293,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 }; } @@ -3292,7 +3308,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; @@ -3355,8 +3371,7 @@ namespace Emby.Server.Implementations.Data { var result = new QueryResult<Guid>(); - var statements = PrepareAllSafe(db, statementTexts) - .ToList(); + var statements = PrepareAllSafe(db, statementTexts); if (!isReturningZeroItems) { @@ -3399,7 +3414,7 @@ namespace Emby.Server.Implementations.Data LogQueryTime("GetItemIds", commandText, now); - result.Items = list.ToArray(); + result.Items = list.ToArray(list.Count); return result; }, ReadTransactionMode); @@ -3604,7 +3619,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)) + ")"); } } @@ -5129,9 +5144,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 +5206,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 +5284,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 +5359,7 @@ namespace Emby.Server.Implementations.Data { result.TotalRecordCount = list.Count; } - result.Items = list.ToArray(); + result.Items = list.ToArray(list.Count); return result; @@ -5354,11 +5368,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; } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 80576742e..098e11720 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -649,12 +649,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 +766,7 @@ namespace Emby.Server.Implementations.Dto } } - dto.People = list.ToArray(); + dto.People = list.ToArray(list.Count); } /// <summary> @@ -1049,12 +1049,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[]{}; } } @@ -1430,9 +1430,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; } } @@ -1581,12 +1581,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 ed69c9755..62f23bb53 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> @@ -40,6 +41,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,6 +55,24 @@ <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\SqliteItemRepository.cs" /> @@ -63,6 +84,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" /> @@ -78,6 +100,7 @@ <Compile Include="FFMpeg\FFMpegInfo.cs" /> <Compile Include="FFMpeg\FFMpegInstallInfo.cs" /> <Compile Include="FFMpeg\FFMpegLoader.cs" /> + <Compile Include="HttpServerFactory.cs" /> <Compile Include="HttpServer\FileWriter.cs" /> <Compile Include="HttpServer\HttpListenerHost.cs" /> <Compile Include="HttpServer\HttpResultFactory.cs" /> @@ -99,7 +122,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" /> @@ -170,6 +195,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" /> @@ -249,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" /> @@ -260,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> @@ -268,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> @@ -280,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> @@ -292,6 +363,15 @@ <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.12.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\ServiceStack.Text.4.5.12\lib\net45\ServiceStack.Text.dll</HintPath> + </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> @@ -299,10 +379,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" /> 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/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/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/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 9588b648e..271dac153 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) { @@ -1298,7 +1273,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 +1292,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 +1316,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 +1490,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) }; } @@ -2610,7 +2587,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) @@ -2856,7 +2833,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..83bee89e0 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) { 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/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index ef32eccea..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; } @@ -271,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..d88076b99 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> @@ -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) @@ -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/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index a277b693a..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; @@ -325,7 +326,7 @@ namespace Emby.Server.Implementations.Library Limit = limit * 5, IsPlayed = isPlayed, DtoOptions = options, - MediaTypes = mediaTypes.ToArray() + 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/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 6173068cc..90121b496 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"; } } 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 9b832bd4a..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> { @@ -1996,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> { @@ -2084,7 +2085,7 @@ namespace Emby.Server.Implementations.LiveTv var returnArray = returnList .OrderBy(i => i.StartDate) - .ToArray(); + .ToArray(returnList.Count); return new QueryResult<TimerInfoDto> { @@ -2341,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); @@ -2705,7 +2706,7 @@ namespace Emby.Server.Implementations.LiveTv return new QueryResult<BaseItemDto> { - Items = groups.ToArray(), + Items = groups.ToArray(groups.Count), TotalRecordCount = groups.Count }; } @@ -2992,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 a7e69776c..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 { @@ -102,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/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/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 2cfa4601a..884f46f1f 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -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"); } 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/ScheduledTasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs index 967e7ddd8..91ae2afd1 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 { @@ -142,7 +143,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 763ca9f24..0b39cfc47 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); } } } 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/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/UserViews/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs index 863391eea..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,7 +36,7 @@ namespace Emby.Server.Implementations.UserViews }; } - protected override List<BaseItem> GetItemsWithImages(IHasImages item) + protected override List<BaseItem> GetItemsWithImages(IHasMetadata item) { var view = (CollectionFolder)item; @@ -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 5230da8a6..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,9 +62,9 @@ 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) || @@ -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..c86812ee0 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.12" 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 |
