diff options
| author | Patrick Barron <barronpm@gmail.com> | 2020-05-15 17:20:07 -0400 |
|---|---|---|
| committer | Patrick Barron <barronpm@gmail.com> | 2020-05-15 17:20:07 -0400 |
| commit | aca7e221d811040bdb14da6390bbd16c5f7db785 (patch) | |
| tree | 87c77521066511890e236a97afb27a475f32c0a3 /Emby.Server.Implementations | |
| parent | b7621d762c58320b35096838ae8ab10d98ea9bb2 (diff) | |
| parent | 652e9702fc67784fd11e26ee159f31e6f0ba9dd7 (diff) | |
Merge branch 'master' into userdb-efcore
# Conflicts:
# Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
# Emby.Server.Implementations/ApplicationHost.cs
# Emby.Server.Implementations/Devices/DeviceManager.cs
# Jellyfin.Server/Jellyfin.Server.csproj
# Jellyfin.Server/Migrations/MigrationRunner.cs
# MediaBrowser.Controller/Devices/IDeviceManager.cs
Diffstat (limited to 'Emby.Server.Implementations')
31 files changed, 424 insertions, 1513 deletions
diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index 3d3859078..31d9609a6 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -8,7 +8,6 @@ using Jellyfin.Data.Entities; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; @@ -30,7 +29,7 @@ namespace Emby.Server.Implementations.Activity /// </summary> public sealed class ActivityLogEntryPoint : IServerEntryPoint { - private readonly ILogger _logger; + private readonly ILogger<ActivityLogEntryPoint> _logger; private readonly IInstallationManager _installationManager; private readonly ISessionManager _sessionManager; private readonly ITaskManager _taskManager; @@ -38,14 +37,12 @@ namespace Emby.Server.Implementations.Activity private readonly ILocalizationManager _localization; private readonly ISubtitleManager _subManager; private readonly IUserManager _userManager; - private readonly IDeviceManager _deviceManager; /// <summary> /// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class. /// </summary> /// <param name="logger">The logger.</param> /// <param name="sessionManager">The session manager.</param> - /// <param name="deviceManager">The device manager.</param> /// <param name="taskManager">The task manager.</param> /// <param name="activityManager">The activity manager.</param> /// <param name="localization">The localization manager.</param> @@ -55,7 +52,6 @@ namespace Emby.Server.Implementations.Activity public ActivityLogEntryPoint( ILogger<ActivityLogEntryPoint> logger, ISessionManager sessionManager, - IDeviceManager deviceManager, ITaskManager taskManager, IActivityManager activityManager, ILocalizationManager localization, @@ -65,7 +61,6 @@ namespace Emby.Server.Implementations.Activity { _logger = logger; _sessionManager = sessionManager; - _deviceManager = deviceManager; _taskManager = taskManager; _activityManager = activityManager; _localization = localization; @@ -98,36 +93,18 @@ namespace Emby.Server.Implementations.Activity _userManager.OnUserDeleted += OnUserDeleted; _userManager.OnUserLockedOut += OnUserLockedOut; - _deviceManager.CameraImageUploaded += OnCameraImageUploaded; - return Task.CompletedTask; } - private async void OnCameraImageUploaded(object sender, GenericEventArgs<CameraImageUploadInfo> e) - { - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("CameraImageUploadedFrom"), - e.Argument.Device.Name), - NotificationType.CameraImageUploaded.ToString(), - Guid.Empty, - DateTime.UtcNow, - LogLevel.Trace)) - .ConfigureAwait(false); - } - private async void OnUserLockedOut(object sender, GenericEventArgs<User> e) { await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserLockedOutWithName"), - e.Argument.Username), - NotificationType.UserLockedOut.ToString(), - e.Argument.Id, - DateTime.UtcNow, - LogLevel.Trace)) + string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("UserLockedOutWithName"), + e.Argument.Username), + NotificationType.UserLockedOut.ToString(), + e.Argument.Id)) .ConfigureAwait(false); } @@ -138,11 +115,9 @@ namespace Emby.Server.Implementations.Activity CultureInfo.InvariantCulture, _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"), e.Provider, - Emby.Notifications.NotificationEntryPoint.GetItemName(e.Item)), + Notifications.NotificationEntryPoint.GetItemName(e.Item)), "SubtitleDownloadFailure", - Guid.Empty, - DateTime.UtcNow, - LogLevel.Trace) + Guid.Empty) { ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture), ShortOverview = e.Exception.Message @@ -180,9 +155,7 @@ namespace Emby.Server.Implementations.Activity GetItemName(item), e.DeviceName), GetPlaybackStoppedNotificationType(item.MediaType), - user.Id, - DateTime.UtcNow, - LogLevel.Trace)) + user.Id)) .ConfigureAwait(false); } @@ -217,9 +190,7 @@ namespace Emby.Server.Implementations.Activity GetItemName(item), e.DeviceName), GetPlaybackNotificationType(item.MediaType), - user.Id, - DateTime.UtcNow, - LogLevel.Trace)) + user.Id)) .ConfigureAwait(false); } @@ -286,9 +257,7 @@ namespace Emby.Server.Implementations.Activity session.UserName, session.DeviceName), "SessionEnded", - session.UserId, - DateTime.UtcNow, - LogLevel.Trace) + session.UserId) { ShortOverview = string.Format( CultureInfo.InvariantCulture, @@ -307,9 +276,7 @@ namespace Emby.Server.Implementations.Activity _localization.GetLocalizedString("AuthenticationSucceededWithUserName"), user.Name), "AuthenticationSucceeded", - user.Id, - DateTime.UtcNow, - LogLevel.Trace) + user.Id) { ShortOverview = string.Format( CultureInfo.InvariantCulture, @@ -326,10 +293,9 @@ namespace Emby.Server.Implementations.Activity _localization.GetLocalizedString("FailedLoginAttemptWithUserName"), e.Argument.Username), "AuthenticationFailed", - Guid.Empty, - DateTime.UtcNow, - LogLevel.Error) + Guid.Empty) { + LogSeverity = LogLevel.Error, ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("LabelIpAddressValue"), @@ -345,9 +311,7 @@ namespace Emby.Server.Implementations.Activity _localization.GetLocalizedString("UserPolicyUpdatedWithName"), e.Argument.Username), "UserPolicyUpdated", - e.Argument.Id, - DateTime.UtcNow, - LogLevel.Trace)) + e.Argument.Id)) .ConfigureAwait(false); } @@ -359,9 +323,7 @@ namespace Emby.Server.Implementations.Activity _localization.GetLocalizedString("UserDeletedWithName"), e.Argument.Username), "UserDeleted", - Guid.Empty, - DateTime.UtcNow, - LogLevel.Trace)) + Guid.Empty)) .ConfigureAwait(false); } @@ -373,9 +335,8 @@ namespace Emby.Server.Implementations.Activity _localization.GetLocalizedString("UserPasswordChangedWithName"), e.Argument.Username), "UserPasswordChanged", - e.Argument.Id, - DateTime.UtcNow, - LogLevel.Trace)).ConfigureAwait(false); + e.Argument.Id)) + .ConfigureAwait(false); } private async void OnUserCreated(object sender, GenericEventArgs<User> e) @@ -386,9 +347,7 @@ namespace Emby.Server.Implementations.Activity _localization.GetLocalizedString("UserCreatedWithName"), e.Argument.Username), "UserCreated", - e.Argument.Id, - DateTime.UtcNow, - LogLevel.Trace)) + e.Argument.Id)) .ConfigureAwait(false); } @@ -408,9 +367,7 @@ namespace Emby.Server.Implementations.Activity session.UserName, session.DeviceName), "SessionStarted", - session.UserId, - DateTime.UtcNow, - LogLevel.Trace) + session.UserId) { ShortOverview = string.Format( CultureInfo.InvariantCulture, @@ -427,9 +384,7 @@ namespace Emby.Server.Implementations.Activity _localization.GetLocalizedString("PluginUpdatedWithName"), e.Argument.Item1.Name), NotificationType.PluginUpdateInstalled.ToString(), - Guid.Empty, - DateTime.UtcNow, - LogLevel.Trace) + Guid.Empty) { ShortOverview = string.Format( CultureInfo.InvariantCulture, @@ -447,9 +402,7 @@ namespace Emby.Server.Implementations.Activity _localization.GetLocalizedString("PluginUninstalledWithName"), e.Argument.Name), NotificationType.PluginUninstalled.ToString(), - Guid.Empty, - DateTime.UtcNow, - LogLevel.Trace)) + Guid.Empty)) .ConfigureAwait(false); } @@ -461,9 +414,7 @@ namespace Emby.Server.Implementations.Activity _localization.GetLocalizedString("PluginInstalledWithName"), e.Argument.name), NotificationType.PluginInstalled.ToString(), - Guid.Empty, - DateTime.UtcNow, - LogLevel.Trace) + Guid.Empty) { ShortOverview = string.Format( CultureInfo.InvariantCulture, @@ -482,9 +433,7 @@ namespace Emby.Server.Implementations.Activity _localization.GetLocalizedString("NameInstallFailed"), installationInfo.Name), NotificationType.InstallationFailed.ToString(), - Guid.Empty, - DateTime.UtcNow, - LogLevel.Trace) + Guid.Empty) { ShortOverview = string.Format( CultureInfo.InvariantCulture, @@ -528,10 +477,9 @@ namespace Emby.Server.Implementations.Activity await CreateLogEntry(new ActivityLog( string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name), NotificationType.TaskFailed.ToString(), - Guid.Empty, - DateTime.UtcNow, - LogLevel.Error) + Guid.Empty) { + LogSeverity = LogLevel.Error, Overview = string.Join(Environment.NewLine, vals), ShortOverview = runningTime }).ConfigureAwait(false); @@ -565,8 +513,6 @@ namespace Emby.Server.Implementations.Activity _userManager.OnUserPasswordChanged -= OnUserPasswordChanged; _userManager.OnUserDeleted -= OnUserDeleted; _userManager.OnUserLockedOut -= OnUserLockedOut; - - _deviceManager.CameraImageUploaded -= OnCameraImageUploaded; } /// <summary> @@ -586,7 +532,7 @@ namespace Emby.Server.Implementations.Activity { int years = days / DaysInYear; values.Add(CreateValueString(years, "year")); - days = days % DaysInYear; + days %= DaysInYear; } // Number of months diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 06cc51c42..b2fe6a3ab 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -43,12 +43,8 @@ using Emby.Server.Implementations.Security; using Emby.Server.Implementations.Serialization; using Emby.Server.Implementations.Services; using Emby.Server.Implementations.Session; -using Emby.Server.Implementations.SocketSharp; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; -using Jellyfin.Server.Implementations; -using Jellyfin.Server.Implementations.Activity; -using Jellyfin.Server.Implementations.User; using MediaBrowser.Api; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; @@ -84,7 +80,6 @@ using MediaBrowser.Controller.Subtitles; using MediaBrowser.Controller.TV; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; -using MediaBrowser.Model.Activity; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dlna; @@ -103,12 +98,10 @@ using MediaBrowser.Providers.Subtitles; using MediaBrowser.WebDashboard.Api; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; using Prometheus.DotNetRuntime; +using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; namespace Emby.Server.Implementations { @@ -505,32 +498,8 @@ namespace Emby.Server.Implementations RegisterServices(serviceCollection); } - public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next) - { - if (!context.WebSockets.IsWebSocketRequest) - { - await next().ConfigureAwait(false); - return; - } - - await _httpServer.ProcessWebSocketRequest(context).ConfigureAwait(false); - } - - public async Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next) - { - if (context.WebSockets.IsWebSocketRequest) - { - await next().ConfigureAwait(false); - return; - } - - var request = context.Request; - var response = context.Response; - var localPath = context.Request.Path.ToString(); - - var req = new WebSocketSharpRequest(request, response, request.Path, LoggerFactory.CreateLogger<WebSocketSharpRequest>()); - await _httpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted).ConfigureAwait(false); - } + public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next) + => _httpServer.RequestHandler(context); /// <summary> /// Registers services/resources with the service collection that will be available via DI. @@ -548,20 +517,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IJsonSerializer, JsonSerializer>(); - // TODO: Remove support for injecting ILogger completely - serviceCollection.AddSingleton((provider) => - { - Logger.LogWarning("Injecting ILogger directly is deprecated and should be replaced with ILogger<T>"); - return Logger; - }); - - // TODO: properly set up scoping and switch to AddDbContextPool - serviceCollection.AddDbContext<JellyfinDb>( - options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"), - ServiceLifetime.Transient); - - serviceCollection.AddSingleton<JellyfinDbProvider>(); - serviceCollection.AddSingleton(_fileSystemManager); serviceCollection.AddSingleton<TvdbClientManager>(); @@ -599,6 +554,8 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>(); serviceCollection.AddSingleton<IUserDataManager, UserDataManager>(); + serviceCollection.AddSingleton<IDisplayPreferencesRepository, SqliteDisplayPreferencesRepository>(); + serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>(); serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>(); @@ -626,7 +583,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<ISearchEngine, SearchEngine>(); serviceCollection.AddSingleton<ServiceController>(); - serviceCollection.AddSingleton<IHttpListener, WebSocketSharpListener>(); serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>(); serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>(); @@ -668,8 +624,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); - serviceCollection.AddSingleton<IActivityManager, ActivityManager>(); - serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>(); serviceCollection.AddSingleton<ISessionContext, SessionContext>(); @@ -697,6 +651,7 @@ namespace Emby.Server.Implementations _httpServer = Resolve<IHttpServer>(); _httpClient = Resolve<IHttpClient>(); + ((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize(); ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize(); SetStaticProperties(); @@ -1147,9 +1102,6 @@ namespace Emby.Server.Implementations ItemsByNamePath = ApplicationPaths.InternalMetadataPath, InternalMetadataPath = ApplicationPaths.InternalMetadataPath, CachePath = ApplicationPaths.CachePath, - HttpServerPortNumber = HttpPort, - SupportsHttps = SupportsHttps, - HttpsPortNumber = HttpsPort, OperatingSystem = OperatingSystem.Id.ToString(), OperatingSystemDisplayName = OperatingSystem.Name, CanSelfRestart = CanSelfRestart, @@ -1185,23 +1137,22 @@ namespace Emby.Server.Implementations }; } - public bool EnableHttps => SupportsHttps && ServerConfigurationManager.Configuration.EnableHttps; - - public bool SupportsHttps => Certificate != null || ServerConfigurationManager.Configuration.IsBehindProxy; + /// <inheritdoc/> + public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps; - public async Task<string> GetLocalApiUrl(CancellationToken cancellationToken, bool forceHttp = false) + /// <inheritdoc/> + public async Task<string> GetLocalApiUrl(CancellationToken cancellationToken) { try { // Return the first matched address, if found, or the first known local address var addresses = await GetLocalIpAddressesInternal(false, 1, cancellationToken).ConfigureAwait(false); - - foreach (var address in addresses) + if (addresses.Count == 0) { - return GetLocalApiUrl(address, forceHttp); + return null; } - return null; + return GetLocalApiUrl(addresses.First()); } catch (Exception ex) { @@ -1228,7 +1179,7 @@ namespace Emby.Server.Implementations } /// <inheritdoc /> - public string GetLocalApiUrl(IPAddress ipAddress, bool forceHttp = false) + public string GetLocalApiUrl(IPAddress ipAddress) { if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6) { @@ -1238,29 +1189,30 @@ namespace Emby.Server.Implementations str.CopyTo(span.Slice(1)); span[^1] = ']'; - return GetLocalApiUrl(span, forceHttp); + return GetLocalApiUrl(span); } - return GetLocalApiUrl(ipAddress.ToString(), forceHttp); + return GetLocalApiUrl(ipAddress.ToString()); } - /// <inheritdoc /> - public string GetLocalApiUrl(ReadOnlySpan<char> host, bool forceHttp = false) + /// <inheritdoc/> + public string GetLoopbackHttpApiUrl() { - var url = new StringBuilder(64); - bool useHttps = EnableHttps && !forceHttp; - url.Append(useHttps ? "https://" : "http://") - .Append(host) - .Append(':') - .Append(useHttps ? HttpsPort : HttpPort); - - string baseUrl = ServerConfigurationManager.Configuration.BaseUrl; - if (baseUrl.Length != 0) - { - url.Append(baseUrl); - } + return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort); + } - return url.ToString(); + /// <inheritdoc/> + public string GetLocalApiUrl(ReadOnlySpan<char> host, string scheme = null, int? port = null) + { + // NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does + // not. For consistency, always trim the trailing slash. + return new UriBuilder + { + Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp), + Host = host.ToString(), + Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort), + Path = ServerConfigurationManager.Configuration.BaseUrl + }.ToString().TrimEnd('/'); } public Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken) @@ -1294,7 +1246,7 @@ namespace Emby.Server.Implementations } } - var valid = await IsIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false); + var valid = await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false); if (valid) { resultList.Add(address); @@ -1328,7 +1280,7 @@ namespace Emby.Server.Implementations private readonly ConcurrentDictionary<string, bool> _validAddressResults = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase); - private async Task<bool> IsIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken) + private async Task<bool> IsLocalIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken) { if (address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback)) @@ -1336,8 +1288,7 @@ namespace Emby.Server.Implementations return true; } - var apiUrl = GetLocalApiUrl(address); - apiUrl += "/system/ping"; + var apiUrl = GetLocalApiUrl(address) + "/system/ping"; if (_validAddressResults.TryGetValue(apiUrl, out var cachedResult)) { diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs index 96096e142..7f7c6a0be 100644 --- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs +++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs @@ -31,18 +31,18 @@ namespace Emby.Server.Implementations.Browser /// Opens the specified URL in an external browser window. Any exceptions will be logged, but ignored. /// </summary> /// <param name="appHost">The application host.</param> - /// <param name="url">The URL.</param> - private static void TryOpenUrl(IServerApplicationHost appHost, string url) + /// <param name="relativeUrl">The URL to open, relative to the server base URL.</param> + private static void TryOpenUrl(IServerApplicationHost appHost, string relativeUrl) { try { string baseUrl = appHost.GetLocalApiUrl("localhost"); - appHost.LaunchUrl(baseUrl + url); + appHost.LaunchUrl(baseUrl + relativeUrl); } catch (Exception ex) { var logger = appHost.Resolve<ILogger>(); - logger?.LogError(ex, "Failed to open browser window with URL {URL}", url); + logger?.LogError(ex, "Failed to open browser window with URL {URL}", relativeUrl); } } } diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index a6eaf2d0a..305e67e8c 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -193,12 +193,6 @@ namespace Emby.Server.Implementations.Configuration changed = true; } - if (!config.CameraUploadUpgraded) - { - config.CameraUploadUpgraded = true; - changed = true; - } - if (!config.CollectionsUpgraded) { config.CollectionsUpgraded = true; diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs index d6bce93f8..2d44a7c28 100644 --- a/Emby.Server.Implementations/Devices/DeviceManager.cs +++ b/Emby.Server.Implementations/Devices/DeviceManager.cs @@ -7,26 +7,19 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Enums; -using MediaBrowser.Common.Configuration; +using Jellyfin.Data.Entities; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Security; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Devices; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Events; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Session; using MediaBrowser.Model.Users; -using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Devices { @@ -34,38 +27,23 @@ namespace Emby.Server.Implementations.Devices { private readonly IJsonSerializer _json; private readonly IUserManager _userManager; - private readonly IFileSystem _fileSystem; - private readonly ILibraryMonitor _libraryMonitor; private readonly IServerConfigurationManager _config; - private readonly ILibraryManager _libraryManager; - private readonly ILocalizationManager _localizationManager; private readonly IAuthenticationRepository _authRepo; private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache; public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated; - public event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded; - - private readonly object _cameraUploadSyncLock = new object(); private readonly object _capabilitiesSyncLock = new object(); public DeviceManager( IAuthenticationRepository authRepo, IJsonSerializer json, - ILibraryManager libraryManager, - ILocalizationManager localizationManager, IUserManager userManager, - IFileSystem fileSystem, - ILibraryMonitor libraryMonitor, IServerConfigurationManager config) { _json = json; _userManager = userManager; - _fileSystem = fileSystem; - _libraryMonitor = libraryMonitor; _config = config; - _libraryManager = libraryManager; - _localizationManager = localizationManager; _authRepo = authRepo; _capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase); } @@ -195,173 +173,7 @@ namespace Emby.Server.Implementations.Devices return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N", CultureInfo.InvariantCulture)); } - public ContentUploadHistory GetCameraUploadHistory(string deviceId) - { - var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); - - lock (_cameraUploadSyncLock) - { - try - { - return _json.DeserializeFromFile<ContentUploadHistory>(path); - } - catch (IOException) - { - return new ContentUploadHistory - { - DeviceId = deviceId - }; - } - } - } - - public async Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file) - { - var device = GetDevice(deviceId, false); - var uploadPathInfo = GetUploadPath(device); - - var path = uploadPathInfo.Item1; - - if (!string.IsNullOrWhiteSpace(file.Album)) - { - path = Path.Combine(path, _fileSystem.GetValidFilename(file.Album)); - } - - path = Path.Combine(path, file.Name); - path = Path.ChangeExtension(path, MimeTypes.ToExtension(file.MimeType) ?? "jpg"); - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - await EnsureLibraryFolder(uploadPathInfo.Item2, uploadPathInfo.Item3).ConfigureAwait(false); - - _libraryMonitor.ReportFileSystemChangeBeginning(path); - - try - { - using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - await stream.CopyToAsync(fs).ConfigureAwait(false); - } - - AddCameraUpload(deviceId, file); - } - finally - { - _libraryMonitor.ReportFileSystemChangeComplete(path, true); - } - - if (CameraImageUploaded != null) - { - CameraImageUploaded?.Invoke(this, new GenericEventArgs<CameraImageUploadInfo> - { - Argument = new CameraImageUploadInfo - { - Device = device, - FileInfo = file - } - }); - } - } - - private void AddCameraUpload(string deviceId, LocalFileInfo file) - { - var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - lock (_cameraUploadSyncLock) - { - ContentUploadHistory history; - - try - { - history = _json.DeserializeFromFile<ContentUploadHistory>(path); - } - catch (IOException) - { - history = new ContentUploadHistory - { - DeviceId = deviceId - }; - } - - history.DeviceId = deviceId; - - var list = history.FilesUploaded.ToList(); - list.Add(file); - history.FilesUploaded = list.ToArray(); - - _json.SerializeToFile(history, path); - } - } - - internal Task EnsureLibraryFolder(string path, string name) - { - var existingFolders = _libraryManager - .RootFolder - .Children - .OfType<Folder>() - .Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path)) - .ToList(); - - if (existingFolders.Count > 0) - { - return Task.CompletedTask; - } - - Directory.CreateDirectory(path); - - var libraryOptions = new LibraryOptions - { - PathInfos = new[] { new MediaPathInfo { Path = path } }, - EnablePhotos = true, - EnableRealtimeMonitor = false, - SaveLocalMetadata = true - }; - - if (string.IsNullOrWhiteSpace(name)) - { - name = _localizationManager.GetLocalizedString("HeaderCameraUploads"); - } - - return _libraryManager.AddVirtualFolder(name, CollectionType.HomeVideos, libraryOptions, true); - } - - private Tuple<string, string, string> GetUploadPath(DeviceInfo device) - { - var config = _config.GetUploadOptions(); - var path = config.CameraUploadPath; - - if (string.IsNullOrWhiteSpace(path)) - { - path = DefaultCameraUploadsPath; - } - - var topLibraryPath = path; - - if (config.EnableCameraUploadSubfolders) - { - path = Path.Combine(path, _fileSystem.GetValidFilename(device.Name)); - } - - return new Tuple<string, string, string>(path, topLibraryPath, null); - } - - internal string GetUploadsPath() - { - var config = _config.GetUploadOptions(); - var path = config.CameraUploadPath; - - if (string.IsNullOrWhiteSpace(path)) - { - path = DefaultCameraUploadsPath; - } - - return path; - } - - private string DefaultCameraUploadsPath => Path.Combine(_config.CommonApplicationPaths.DataPath, "camerauploads"); - - public bool CanAccessDevice(Jellyfin.Data.Entities.User user, string deviceId) + public bool CanAccessDevice(User user, string deviceId) { if (user == null) { @@ -391,102 +203,4 @@ namespace Emby.Server.Implementations.Devices return true; } } - - public class DeviceManagerEntryPoint : IServerEntryPoint - { - private readonly DeviceManager _deviceManager; - private readonly IServerConfigurationManager _config; - private ILogger _logger; - - public DeviceManagerEntryPoint( - IDeviceManager deviceManager, - IServerConfigurationManager config, - ILogger<DeviceManagerEntryPoint> logger) - { - _deviceManager = (DeviceManager)deviceManager; - _config = config; - _logger = logger; - } - - public async Task RunAsync() - { - if (!_config.Configuration.CameraUploadUpgraded && _config.Configuration.IsStartupWizardCompleted) - { - var path = _deviceManager.GetUploadsPath(); - - if (Directory.Exists(path)) - { - try - { - await _deviceManager.EnsureLibraryFolder(path, null).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating camera uploads library"); - } - - _config.Configuration.CameraUploadUpgraded = true; - _config.SaveConfiguration(); - } - } - } - - #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // TODO: dispose managed state (managed objects). - } - - // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. - // TODO: set large fields to null. - - disposedValue = true; - } - } - - // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. - // ~DeviceManagerEntryPoint() { - // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - // Dispose(false); - // } - - // This code added to correctly implement the disposable pattern. - public void Dispose() - { - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - Dispose(true); - // TODO: uncomment the following line if the finalizer is overridden above. - // GC.SuppressFinalize(this); - } - #endregion - } - - public class DevicesConfigStore : IConfigurationFactory - { - public IEnumerable<ConfigurationStore> GetConfigurations() - { - return new ConfigurationStore[] - { - new ConfigurationStore - { - Key = "devices", - ConfigurationType = typeof(DevicesOptions) - } - }; - } - } - - public static class UploadConfigExtension - { - public static DevicesOptions GetUploadOptions(this IConfigurationManager config) - { - return config.GetConfiguration<DevicesOptions>("devices"); - } - } } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index dccbe2a9a..896e4310e 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -9,7 +9,6 @@ <ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" /> <ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" /> <ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" /> - <ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" /> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> @@ -51,7 +50,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netcoreapp3.1</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index 37d7fd479..878cee23c 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.EntryPoints .Append(config.PublicHttpsPort).Append(Separator) .Append(_appHost.HttpPort).Append(Separator) .Append(_appHost.HttpsPort).Append(Separator) - .Append(_appHost.EnableHttps).Append(Separator) + .Append(_appHost.ListenWithHttps).Append(Separator) .Append(config.EnableRemoteAccess).Append(Separator) .ToString(); } @@ -158,7 +158,7 @@ namespace Emby.Server.Implementations.EntryPoints { yield return CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort); - if (_appHost.EnableHttps) + if (_appHost.ListenWithHttps) { yield return CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort); } diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 211a0c1d9..794d55c04 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -6,11 +6,12 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Sockets; +using System.Net.WebSockets; using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Services; +using Emby.Server.Implementations.SocketSharp; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -22,15 +23,17 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; using ServiceStack.Text.Jsv; namespace Emby.Server.Implementations.HttpServer { - public class HttpListenerHost : IHttpServer, IDisposable + public class HttpListenerHost : IHttpServer { /// <summary> /// The key for a setting that specifies the default redirect path @@ -39,17 +42,17 @@ namespace Emby.Server.Implementations.HttpServer public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath"; private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; private readonly IServerConfigurationManager _config; private readonly INetworkManager _networkManager; private readonly IServerApplicationHost _appHost; private readonly IJsonSerializer _jsonSerializer; private readonly IXmlSerializer _xmlSerializer; - private readonly IHttpListener _socketListener; private readonly Func<Type, Func<string, object>> _funcParseFn; private readonly string _defaultRedirectPath; private readonly string _baseUrlPrefix; + private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>(); - private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>(); private readonly IHostEnvironment _hostEnvironment; private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>(); @@ -63,10 +66,10 @@ namespace Emby.Server.Implementations.HttpServer INetworkManager networkManager, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, - IHttpListener socketListener, ILocalizationManager localizationManager, ServiceController serviceController, - IHostEnvironment hostEnvironment) + IHostEnvironment hostEnvironment, + ILoggerFactory loggerFactory) { _appHost = applicationHost; _logger = logger; @@ -76,11 +79,9 @@ namespace Emby.Server.Implementations.HttpServer _networkManager = networkManager; _jsonSerializer = jsonSerializer; _xmlSerializer = xmlSerializer; - _socketListener = socketListener; ServiceController = serviceController; - - _socketListener.WebSocketConnected = OnWebSocketConnected; _hostEnvironment = hostEnvironment; + _loggerFactory = loggerFactory; _funcParseFn = t => s => JsvReader.GetParseFn(t)(s); @@ -172,38 +173,6 @@ namespace Emby.Server.Implementations.HttpServer return attributes; } - private void OnWebSocketConnected(WebSocketConnectEventArgs e) - { - if (_disposed) - { - return; - } - - var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger) - { - OnReceive = ProcessWebSocketMessageReceived, - Url = e.Url, - QueryString = e.QueryString - }; - - connection.Closed += OnConnectionClosed; - - lock (_webSocketConnections) - { - _webSocketConnections.Add(connection); - } - - WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection)); - } - - private void OnConnectionClosed(object sender, EventArgs e) - { - lock (_webSocketConnections) - { - _webSocketConnections.Remove((IWebSocketConnection)sender); - } - } - private static Exception GetActualException(Exception ex) { if (ex is AggregateException agg) @@ -289,32 +258,6 @@ namespace Emby.Server.Implementations.HttpServer .Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase); } - /// <summary> - /// Shut down the Web Service - /// </summary> - public void Stop() - { - List<IWebSocketConnection> connections; - - lock (_webSocketConnections) - { - connections = _webSocketConnections.ToList(); - _webSocketConnections.Clear(); - } - - foreach (var connection in connections) - { - try - { - connection.Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error disposing connection"); - } - } - } - public static string RemoveQueryStringByKey(string url, string key) { var uri = new Uri(url); @@ -424,33 +367,52 @@ namespace Emby.Server.Implementations.HttpServer return true; } + /// <summary> + /// Validate a connection from a remote IP address to a URL to see if a redirection to HTTPS is required. + /// </summary> + /// <returns>True if the request is valid, or false if the request is not valid and an HTTPS redirect is required.</returns> private bool ValidateSsl(string remoteIp, string urlString) { - if (_config.Configuration.RequireHttps && _appHost.EnableHttps && !_config.Configuration.IsBehindProxy) + if (_config.Configuration.RequireHttps + && _appHost.ListenWithHttps + && !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase)) { - if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1) + // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected + if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1 + || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1) { - // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected - if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1 - || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1) - { - return true; - } + return true; + } - if (!_networkManager.IsInLocalNetwork(remoteIp)) - { - return false; - } + if (!_networkManager.IsInLocalNetwork(remoteIp)) + { + return false; } } return true; } + /// <inheritdoc /> + public Task RequestHandler(HttpContext context) + { + if (context.WebSockets.IsWebSocketRequest) + { + return WebSocketRequestHandler(context); + } + + var request = context.Request; + var response = context.Response; + var localPath = context.Request.Path.ToString(); + + var req = new WebSocketSharpRequest(request, response, request.Path, _logger); + return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted); + } + /// <summary> /// Overridable method that can be used to implement a custom handler. /// </summary> - public async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken) + private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken) { var stopWatch = new Stopwatch(); stopWatch.Start(); @@ -493,9 +455,10 @@ namespace Emby.Server.Implementations.HttpServer if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase)) { httpRes.StatusCode = 200; - httpRes.Headers.Add("Access-Control-Allow-Origin", "*"); - httpRes.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); - httpRes.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization"); + foreach(var (key, value) in GetDefaultCorsHeaders(httpReq)) + { + httpRes.Headers.Add(key, value); + } httpRes.ContentType = "text/plain"; await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false); return; @@ -578,6 +541,68 @@ namespace Emby.Server.Implementations.HttpServer } } + private async Task WebSocketRequestHandler(HttpContext context) + { + if (_disposed) + { + return; + } + + try + { + _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress); + + WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); + + var connection = new WebSocketConnection( + _loggerFactory.CreateLogger<WebSocketConnection>(), + webSocket, + context.Connection.RemoteIpAddress, + context.Request.Query) + { + OnReceive = ProcessWebSocketMessageReceived + }; + + WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection)); + + await connection.ProcessAsync().ConfigureAwait(false); + _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress); + } + catch (Exception ex) // Otherwise ASP.Net will ignore the exception + { + _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress); + if (!context.Response.HasStarted) + { + context.Response.StatusCode = 500; + } + } + } + + /// <summary> + /// Get the default CORS headers + /// </summary> + /// <param name="req"></param> + /// <returns></returns> + public IDictionary<string, string> GetDefaultCorsHeaders(IRequest req) + { + var origin = req.Headers["Origin"]; + if (origin == StringValues.Empty) + { + origin = req.Headers["Host"]; + if (origin == StringValues.Empty) + { + origin = "*"; + } + } + + var headers = new Dictionary<string, string>(); + headers.Add("Access-Control-Allow-Origin", origin); + headers.Add("Access-Control-Allow-Credentials", "true"); + headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); + headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie"); + return headers; + } + // Entry point for HttpListener public ServiceHandler GetServiceHandler(IHttpRequest httpReq) { @@ -624,7 +649,7 @@ namespace Emby.Server.Implementations.HttpServer ResponseFilters = new Action<IRequest, HttpResponse, object>[] { - new ResponseFilter(_logger).FilterResponse + new ResponseFilter(this, _logger).FilterResponse }; } @@ -685,11 +710,6 @@ namespace Emby.Server.Implementations.HttpServer return _jsonSerializer.DeserializeFromStreamAsync(stream, type); } - public Task ProcessWebSocketRequest(HttpContext context) - { - return _socketListener.ProcessWebSocketRequest(context); - } - private string NormalizeEmbyRoutePath(string path) { _logger.LogDebug("Normalizing /emby route"); @@ -708,28 +728,6 @@ namespace Emby.Server.Implementations.HttpServer return _baseUrlPrefix + NormalizeUrlPath(path); } - /// <inheritdoc /> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - Stop(); - } - - _disposed = true; - } - /// <summary> /// Processes the web socket message received. /// </summary> @@ -741,8 +739,6 @@ namespace Emby.Server.Implementations.HttpServer return Task.CompletedTask; } - _logger.LogDebug("Websocket message received: {0}", result.MessageType); - IEnumerable<Task> GetTasks() { foreach (var x in _webSocketListeners) diff --git a/Emby.Server.Implementations/HttpServer/IHttpListener.cs b/Emby.Server.Implementations/HttpServer/IHttpListener.cs deleted file mode 100644 index 501593725..000000000 --- a/Emby.Server.Implementations/HttpServer/IHttpListener.cs +++ /dev/null @@ -1,39 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.Net; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; - -namespace Emby.Server.Implementations.HttpServer -{ - public interface IHttpListener : IDisposable - { - /// <summary> - /// Gets or sets the error handler. - /// </summary> - /// <value>The error handler.</value> - Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; } - - /// <summary> - /// Gets or sets the request handler. - /// </summary> - /// <value>The request handler.</value> - Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; } - - /// <summary> - /// Gets or sets the web socket handler. - /// </summary> - /// <value>The web socket handler.</value> - Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; } - - /// <summary> - /// Stops this instance. - /// </summary> - Task Stop(); - - Task ProcessWebSocketRequest(HttpContext ctx); - } -} diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs index 4089aa578..85c3db9b2 100644 --- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs +++ b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Text; +using MediaBrowser.Controller.Net; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -13,14 +15,17 @@ namespace Emby.Server.Implementations.HttpServer /// </summary> public class ResponseFilter { + private readonly IHttpServer _server; private readonly ILogger _logger; /// <summary> /// Initializes a new instance of the <see cref="ResponseFilter"/> class. /// </summary> + /// <param name="server">The HTTP server.</param> /// <param name="logger">The logger.</param> - public ResponseFilter(ILogger logger) + public ResponseFilter(IHttpServer server, ILogger logger) { + _server = server; _logger = logger; } @@ -32,10 +37,16 @@ namespace Emby.Server.Implementations.HttpServer /// <param name="dto">The dto.</param> public void FilterResponse(IRequest req, HttpResponse res, object dto) { + foreach(var (key, value) in _server.GetDefaultCorsHeaders(req)) + { + res.Headers.Add(key, value); + } // Try to prevent compatibility view - res.Headers.Add("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization"); - res.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); - res.Headers.Add("Access-Control-Allow-Origin", "*"); + res.Headers["Access-Control-Allow-Headers"] = ("Accept, Accept-Language, Authorization, Cache-Control, " + + "Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, " + + "Content-Type, Cookie, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, " + + "Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, " + + "X-Emby-Authorization"); if (dto is Exception exception) { diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index 2292d86a4..095725c50 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -1,15 +1,18 @@ -using System; +#nullable enable + +using System; +using System.Buffers; +using System.IO.Pipelines; +using System.Net; using System.Net.WebSockets; -using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Emby.Server.Implementations.Net; +using MediaBrowser.Common.Json; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Net; -using MediaBrowser.Model.Serialization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -using UtfUnknown; namespace Emby.Server.Implementations.HttpServer { @@ -24,69 +27,50 @@ namespace Emby.Server.Implementations.HttpServer private readonly ILogger _logger; /// <summary> - /// The json serializer. + /// The json serializer options. /// </summary> - private readonly IJsonSerializer _jsonSerializer; + private readonly JsonSerializerOptions _jsonOptions; /// <summary> /// The socket. /// </summary> - private readonly IWebSocket _socket; + private readonly WebSocket _socket; /// <summary> /// Initializes a new instance of the <see cref="WebSocketConnection" /> class. /// </summary> + /// <param name="logger">The logger.</param> /// <param name="socket">The socket.</param> /// <param name="remoteEndPoint">The remote end point.</param> - /// <param name="jsonSerializer">The json serializer.</param> - /// <param name="logger">The logger.</param> - /// <exception cref="ArgumentNullException">socket</exception> - public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger) + /// <param name="query">The query.</param> + public WebSocketConnection( + ILogger<WebSocketConnection> logger, + WebSocket socket, + IPAddress? remoteEndPoint, + IQueryCollection query) { - if (socket == null) - { - throw new ArgumentNullException(nameof(socket)); - } - - if (string.IsNullOrEmpty(remoteEndPoint)) - { - throw new ArgumentNullException(nameof(remoteEndPoint)); - } - - if (jsonSerializer == null) - { - throw new ArgumentNullException(nameof(jsonSerializer)); - } - - if (logger == null) - { - throw new ArgumentNullException(nameof(logger)); - } - - Id = Guid.NewGuid(); - _jsonSerializer = jsonSerializer; + _logger = logger; _socket = socket; - _socket.OnReceiveBytes = OnReceiveInternal; - RemoteEndPoint = remoteEndPoint; - _logger = logger; + QueryString = query; - socket.Closed += OnSocketClosed; + _jsonOptions = JsonDefaults.GetOptions(); + LastActivityDate = DateTime.Now; } /// <inheritdoc /> - public event EventHandler<EventArgs> Closed; + public event EventHandler<EventArgs>? Closed; /// <summary> /// Gets or sets the remote end point. /// </summary> - public string RemoteEndPoint { get; private set; } + public IPAddress? RemoteEndPoint { get; } /// <summary> /// Gets or sets the receive action. /// </summary> /// <value>The receive action.</value> - public Func<WebSocketMessageInfo, Task> OnReceive { get; set; } + public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; } /// <summary> /// Gets the last activity date. @@ -95,22 +79,10 @@ namespace Emby.Server.Implementations.HttpServer public DateTime LastActivityDate { get; private set; } /// <summary> - /// Gets the id. - /// </summary> - /// <value>The id.</value> - public Guid Id { get; private set; } - - /// <summary> - /// Gets or sets the URL. - /// </summary> - /// <value>The URL.</value> - public string Url { get; set; } - - /// <summary> /// Gets or sets the query string. /// </summary> /// <value>The query string.</value> - public IQueryCollection QueryString { get; set; } + public IQueryCollection QueryString { get; } /// <summary> /// Gets the state. @@ -118,138 +90,135 @@ namespace Emby.Server.Implementations.HttpServer /// <value>The state.</value> public WebSocketState State => _socket.State; - void OnSocketClosed(object sender, EventArgs e) + /// <summary> + /// Sends a message asynchronously. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="message">The message.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken) { - Closed?.Invoke(this, EventArgs.Empty); + var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions); + return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken); } - /// <summary> - /// Called when [receive]. - /// </summary> - /// <param name="bytes">The bytes.</param> - private void OnReceiveInternal(byte[] bytes) + /// <inheritdoc /> + public async Task ProcessAsync(CancellationToken cancellationToken = default) { - LastActivityDate = DateTime.UtcNow; + var pipe = new Pipe(); + var writer = pipe.Writer; - if (OnReceive == null) + ValueWebSocketReceiveResult receiveresult; + do { - return; - } - var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName; + // Allocate at least 512 bytes from the PipeWriter + Memory<byte> memory = writer.GetMemory(512); + try + { + receiveresult = await _socket.ReceiveAsync(memory, cancellationToken); + } + catch (WebSocketException ex) + { + _logger.LogWarning("WS {IP} error receiving data: {Message}", RemoteEndPoint, ex.Message); + break; + } - if (string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase)) - { - OnReceiveInternal(Encoding.UTF8.GetString(bytes, 0, bytes.Length)); - } - else + int bytesRead = receiveresult.Count; + if (bytesRead == 0) + { + break; + } + + // Tell the PipeWriter how much was read from the Socket + writer.Advance(bytesRead); + + // Make the data available to the PipeReader + FlushResult flushResult = await writer.FlushAsync(); + if (flushResult.IsCompleted) + { + // The PipeReader stopped reading + break; + } + + LastActivityDate = DateTime.UtcNow; + + if (receiveresult.EndOfMessage) + { + await ProcessInternal(pipe.Reader).ConfigureAwait(false); + } + } while ( + (_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting) + && receiveresult.MessageType != WebSocketMessageType.Close); + + Closed?.Invoke(this, EventArgs.Empty); + + if (_socket.State == WebSocketState.Open + || _socket.State == WebSocketState.CloseReceived + || _socket.State == WebSocketState.CloseSent) { - OnReceiveInternal(Encoding.ASCII.GetString(bytes, 0, bytes.Length)); + await _socket.CloseAsync( + WebSocketCloseStatus.NormalClosure, + string.Empty, + cancellationToken).ConfigureAwait(false); } } - private void OnReceiveInternal(string message) + private async Task ProcessInternal(PipeReader reader) { - LastActivityDate = DateTime.UtcNow; - - if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase)) - { - // This info is useful sometimes but also clogs up the log - _logger.LogDebug("Received web socket message that is not a json structure: {message}", message); - return; - } + ReadResult result = await reader.ReadAsync().ConfigureAwait(false); + ReadOnlySequence<byte> buffer = result.Buffer; if (OnReceive == null) { + // Tell the PipeReader how much of the buffer we have consumed + reader.AdvanceTo(buffer.End); return; } + WebSocketMessage<object> stub; try { - var stub = (WebSocketMessage<object>)_jsonSerializer.DeserializeFromString(message, typeof(WebSocketMessage<object>)); - var info = new WebSocketMessageInfo + if (buffer.IsSingleSegment) { - MessageType = stub.MessageType, - Data = stub.Data?.ToString(), - Connection = this - }; - - OnReceive(info); + stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buffer.FirstSpan, _jsonOptions); + } + else + { + var buf = ArrayPool<byte>.Shared.Rent(Convert.ToInt32(buffer.Length)); + try + { + buffer.CopyTo(buf); + stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buf, _jsonOptions); + } + finally + { + ArrayPool<byte>.Shared.Return(buf); + } + } } - catch (Exception ex) + catch (JsonException ex) { + // Tell the PipeReader how much of the buffer we have consumed + reader.AdvanceTo(buffer.End); _logger.LogError(ex, "Error processing web socket message"); - } - } - - /// <summary> - /// Sends a message asynchronously. - /// </summary> - /// <typeparam name="T"></typeparam> - /// <param name="message">The message.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">message</exception> - public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken) - { - if (message == null) - { - throw new ArgumentNullException(nameof(message)); - } - - var json = _jsonSerializer.SerializeToString(message); - - return SendAsync(json, cancellationToken); - } - - /// <summary> - /// Sends a message asynchronously. - /// </summary> - /// <param name="buffer">The buffer.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendAsync(byte[] buffer, CancellationToken cancellationToken) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); + return; } - cancellationToken.ThrowIfCancellationRequested(); + // Tell the PipeReader how much of the buffer we have consumed + reader.AdvanceTo(buffer.End); - return _socket.SendAsync(buffer, true, cancellationToken); - } + _logger.LogDebug("WS {IP} received message: {@Message}", RemoteEndPoint, stub); - /// <inheritdoc /> - public Task SendAsync(string text, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(text)) + var info = new WebSocketMessageInfo { - throw new ArgumentNullException(nameof(text)); - } + MessageType = stub.MessageType, + Data = stub.Data?.ToString(), // Data can be null + Connection = this + }; - cancellationToken.ThrowIfCancellationRequested(); - - return _socket.SendAsync(text, true, cancellationToken); - } - - /// <inheritdoc /> - public void Dispose() - { - 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) - { - _socket.Dispose(); - } + await OnReceive(info).ConfigureAwait(false); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index 85b1b6e32..6c9ba7c27 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// </summary> public class MusicAlbumResolver : ItemResolver<MusicAlbum> { - private readonly ILogger _logger; + private readonly ILogger<MusicAlbumResolver> _logger; private readonly IFileSystem _fileSystem; private readonly ILibraryManager _libraryManager; @@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// <param name="logger">The logger.</param> /// <param name="fileSystem">The file system.</param> /// <param name="libraryManager">The library manager.</param> - public MusicAlbumResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager) + public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager) { _logger = logger; _fileSystem = fileSystem; diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index 681db4896..5f5cd0e92 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// </summary> public class MusicArtistResolver : ItemResolver<MusicArtist> { - private readonly ILogger _logger; + private readonly ILogger<MusicAlbumResolver> _logger; private readonly IFileSystem _fileSystem; private readonly ILibraryManager _libraryManager; private readonly IServerConfigurationManager _config; @@ -23,12 +23,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// <summary> /// Initializes a new instance of the <see cref="MusicArtistResolver"/> class. /// </summary> - /// <param name="logger">The logger.</param> + /// <param name="logger">The logger for the created <see cref="MusicAlbumResolver"/> instances.</param> /// <param name="fileSystem">The file system.</param> /// <param name="libraryManager">The library manager.</param> /// <param name="config">The configuration manager.</param> public MusicArtistResolver( - ILogger<MusicArtistResolver> logger, + ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager, IServerConfigurationManager config) diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 33f4ca146..3efe1ee25 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -1059,7 +1059,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var stream = new MediaSourceInfo { - EncoderPath = _appHost.GetLocalApiUrl("127.0.0.1", true) + "/LiveTv/LiveRecordings/" + info.Id + "/stream", + EncoderPath = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveRecordings/" + info.Id + "/stream", EncoderProtocol = MediaProtocol.Http, Path = info.Path, Protocol = MediaProtocol.File, diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index d89a816b3..82b1f3cf1 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun //OpenedMediaSource.Path = tempFile; //OpenedMediaSource.ReadAtNativeFramerate = true; - MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1", true) + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; + MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; MediaSource.Protocol = MediaProtocol.Http; //OpenedMediaSource.SupportsDirectPlay = false; //OpenedMediaSource.SupportsDirectStream = true; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index f5dda79db..f7c9c736e 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public M3UTunerHost( IServerConfigurationManager config, IMediaSourceManager mediaSourceManager, - ILogger logger, + ILogger<M3UTunerHost> logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IHttpClient httpClient, @@ -83,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return Task.FromResult(list); } - private static readonly string[] _disallowedSharedStreamExtensions = new string[] + private static readonly string[] _disallowedSharedStreamExtensions = { ".mkv", ".mp4", diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index 0e600202a..083fcd029 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -106,7 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts //OpenedMediaSource.Path = tempFile; //OpenedMediaSource.ReadAtNativeFramerate = true; - MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1", true) + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; + MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; MediaSource.Protocol = MediaProtocol.Http; //OpenedMediaSource.Path = TempFilePath; diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json index ef7792356..4949b10e6 100644 --- a/Emby.Server.Implementations/Localization/Core/bn.json +++ b/Emby.Server.Implementations/Localization/Core/bn.json @@ -91,5 +91,7 @@ "HeaderNextUp": "এরপরে আসছে", "HeaderLiveTV": "লাইভ টিভি", "HeaderFavoriteSongs": "প্রিয় গানগুলো", - "HeaderFavoriteShows": "প্রিয় শোগুলো" + "HeaderFavoriteShows": "প্রিয় শোগুলো", + "TasksLibraryCategory": "গ্রন্থাগার", + "TasksMaintenanceCategory": "রক্ষণাবেক্ষণ" } diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index 8abe31d2a..4e54b9f7a 100644 --- a/Emby.Server.Implementations/Localization/Core/he.json +++ b/Emby.Server.Implementations/Localization/Core/he.json @@ -99,5 +99,13 @@ "TaskCleanCache": "נקה תיקיית מטמון", "TasksApplicationCategory": "יישום", "TasksLibraryCategory": "ספרייה", - "TasksMaintenanceCategory": "תחזוקה" + "TasksMaintenanceCategory": "תחזוקה", + "TaskUpdatePlugins": "עדכן תוספים", + "TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.", + "TaskRefreshPeople": "רענן אנשים", + "TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.", + "TaskCleanLogs": "נקה תיקיית יומן", + "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.", + "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.", + "TasksChannelsCategory": "ערוצי אינטרנט" } diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index 50d0d083c..5637ce346 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -97,5 +97,9 @@ "TasksApplicationCategory": "Applikasjon", "TasksLibraryCategory": "Bibliotek", "TasksMaintenanceCategory": "Vedlikehold", - "TaskCleanCache": "Tøm buffer katalog" + "TaskCleanCache": "Tøm buffer katalog", + "TaskRefreshLibrary": "Skann mediebibliotek", + "TaskRefreshChapterImagesDescription": "Lager forhåndsvisningsbilder for videoer som har kapitler.", + "TaskRefreshChapterImages": "Trekk ut Kapittelbilder", + "TaskCleanCacheDescription": "Sletter mellomlagrede filer som ikke lengre trengs av systemet." } diff --git a/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs b/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs deleted file mode 100644 index fda32da5e..000000000 --- a/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using WebSocketManager = Emby.Server.Implementations.WebSockets.WebSocketManager; - -namespace Emby.Server.Implementations.Middleware -{ - public class WebSocketMiddleware - { - private readonly RequestDelegate _next; - private readonly ILogger<WebSocketMiddleware> _logger; - private readonly WebSocketManager _webSocketManager; - - public WebSocketMiddleware(RequestDelegate next, ILogger<WebSocketMiddleware> logger, WebSocketManager webSocketManager) - { - _next = next; - _logger = logger; - _webSocketManager = webSocketManager; - } - - public async Task Invoke(HttpContext httpContext) - { - _logger.LogInformation("Handling request: " + httpContext.Request.Path); - - if (httpContext.WebSockets.IsWebSocketRequest) - { - var webSocketContext = await httpContext.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false); - if (webSocketContext != null) - { - await _webSocketManager.OnWebSocketConnected(webSocketContext).ConfigureAwait(false); - } - } - else - { - await _next.Invoke(httpContext).ConfigureAwait(false); - } - } - } -} diff --git a/Emby.Server.Implementations/Net/IWebSocket.cs b/Emby.Server.Implementations/Net/IWebSocket.cs deleted file mode 100644 index 4d160aa66..000000000 --- a/Emby.Server.Implementations/Net/IWebSocket.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Net.WebSockets; -using System.Threading; -using System.Threading.Tasks; - -namespace Emby.Server.Implementations.Net -{ - /// <summary> - /// Interface IWebSocket - /// </summary> - public interface IWebSocket : IDisposable - { - /// <summary> - /// Occurs when [closed]. - /// </summary> - event EventHandler<EventArgs> Closed; - - /// <summary> - /// Gets or sets the state. - /// </summary> - /// <value>The state.</value> - WebSocketState State { get; } - - /// <summary> - /// Gets or sets the receive action. - /// </summary> - /// <value>The receive action.</value> - Action<byte[]> OnReceiveBytes { get; set; } - - /// <summary> - /// Sends the async. - /// </summary> - /// <param name="bytes">The bytes.</param> - /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken); - - /// <summary> - /// Sends the asynchronous. - /// </summary> - /// <param name="text">The text.</param> - /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken); - } -} diff --git a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs b/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs deleted file mode 100644 index 6880766f9..000000000 --- a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using Microsoft.AspNetCore.Http; - -namespace Emby.Server.Implementations.Net -{ - public class WebSocketConnectEventArgs : EventArgs - { - /// <summary> - /// Gets or sets the URL. - /// </summary> - /// <value>The URL.</value> - public string Url { get; set; } - /// <summary> - /// Gets or sets the query string. - /// </summary> - /// <value>The query string.</value> - public IQueryCollection QueryString { get; set; } - /// <summary> - /// Gets or sets the web socket. - /// </summary> - /// <value>The web socket.</value> - public IWebSocket WebSocket { get; set; } - /// <summary> - /// Gets or sets the endpoint. - /// </summary> - /// <value>The endpoint.</value> - public string Endpoint { get; set; } - } -} diff --git a/Emby.Server.Implementations/Session/HttpSessionController.cs b/Emby.Server.Implementations/Session/HttpSessionController.cs deleted file mode 100644 index dfb81816c..000000000 --- a/Emby.Server.Implementations/Session/HttpSessionController.cs +++ /dev/null @@ -1,191 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Session; - -namespace Emby.Server.Implementations.Session -{ - public class HttpSessionController : ISessionController - { - private readonly IHttpClient _httpClient; - private readonly IJsonSerializer _json; - private readonly ISessionManager _sessionManager; - - public SessionInfo Session { get; private set; } - - private readonly string _postUrl; - - public HttpSessionController(IHttpClient httpClient, - IJsonSerializer json, - SessionInfo session, - string postUrl, ISessionManager sessionManager) - { - _httpClient = httpClient; - _json = json; - Session = session; - _postUrl = postUrl; - _sessionManager = sessionManager; - } - - private string PostUrl => string.Format("http://{0}{1}", Session.RemoteEndPoint, _postUrl); - - public bool IsSessionActive => (DateTime.UtcNow - Session.LastActivityDate).TotalMinutes <= 5; - - public bool SupportsMediaControl => true; - - private Task SendMessage(string name, string messageId, CancellationToken cancellationToken) - { - return SendMessage(name, messageId, new Dictionary<string, string>(), cancellationToken); - } - - private Task SendMessage(string name, string messageId, Dictionary<string, string> args, CancellationToken cancellationToken) - { - args["messageId"] = messageId; - var url = PostUrl + "/" + name + ToQueryString(args); - - return SendRequest(new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - BufferContent = false - }); - } - - private Task SendPlayCommand(PlayRequest command, string messageId, CancellationToken cancellationToken) - { - var dict = new Dictionary<string, string>(); - - dict["ItemIds"] = string.Join(",", command.ItemIds.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray()); - - if (command.StartPositionTicks.HasValue) - { - dict["StartPositionTicks"] = command.StartPositionTicks.Value.ToString(CultureInfo.InvariantCulture); - } - if (command.AudioStreamIndex.HasValue) - { - dict["AudioStreamIndex"] = command.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture); - } - if (command.SubtitleStreamIndex.HasValue) - { - dict["SubtitleStreamIndex"] = command.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture); - } - if (command.StartIndex.HasValue) - { - dict["StartIndex"] = command.StartIndex.Value.ToString(CultureInfo.InvariantCulture); - } - if (!string.IsNullOrEmpty(command.MediaSourceId)) - { - dict["MediaSourceId"] = command.MediaSourceId; - } - - return SendMessage(command.PlayCommand.ToString(), messageId, dict, cancellationToken); - } - - private Task SendPlaystateCommand(PlaystateRequest command, string messageId, CancellationToken cancellationToken) - { - var args = new Dictionary<string, string>(); - - if (command.Command == PlaystateCommand.Seek) - { - if (!command.SeekPositionTicks.HasValue) - { - throw new ArgumentException("SeekPositionTicks cannot be null"); - } - - args["SeekPositionTicks"] = command.SeekPositionTicks.Value.ToString(CultureInfo.InvariantCulture); - } - - return SendMessage(command.Command.ToString(), messageId, args, cancellationToken); - } - - private string[] _supportedMessages = Array.Empty<string>(); - public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken) - { - if (!IsSessionActive) - { - return Task.CompletedTask; - } - - if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase)) - { - return SendPlayCommand(data as PlayRequest, messageId, cancellationToken); - } - if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase)) - { - return SendPlaystateCommand(data as PlaystateRequest, messageId, cancellationToken); - } - if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase)) - { - var command = data as GeneralCommand; - return SendMessage(command.Name, messageId, command.Arguments, cancellationToken); - } - - if (!_supportedMessages.Contains(name, StringComparer.OrdinalIgnoreCase)) - { - return Task.CompletedTask; - } - - var url = PostUrl + "/" + name; - - url += "?messageId=" + messageId; - - var options = new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - BufferContent = false - }; - - if (data != null) - { - if (typeof(T) == typeof(string)) - { - var str = data as string; - if (!string.IsNullOrEmpty(str)) - { - options.RequestContent = str; - options.RequestContentType = "application/json"; - } - } - else - { - options.RequestContent = _json.SerializeToString(data); - options.RequestContentType = "application/json"; - } - } - - return SendRequest(options); - } - - private async Task SendRequest(HttpRequestOptions options) - { - using (var response = await _httpClient.Post(options).ConfigureAwait(false)) - { - - } - } - - private static string ToQueryString(Dictionary<string, string> nvc) - { - var array = (from item in nvc - select string.Format("{0}={1}", WebUtility.UrlEncode(item.Key), WebUtility.UrlEncode(item.Value))) - .ToArray(); - - var args = string.Join("&", array); - - if (string.IsNullOrEmpty(args)) - { - return args; - } - - return "?" + args; - } - } -} diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index d595e96bd..84b16f1c7 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -490,8 +490,7 @@ namespace Emby.Server.Implementations.Session Client = appName, DeviceId = deviceId, ApplicationVersion = appVersion, - Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture), - ServerId = _appHost.SystemId + Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture) }; var username = user?.Username; @@ -1052,12 +1051,12 @@ namespace Emby.Server.Implementations.Session private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken) { - var controllers = session.SessionControllers.ToArray(); - var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + var controllers = session.SessionControllers; + var messageId = Guid.NewGuid(); foreach (var controller in controllers) { - await controller.SendMessage(name, messageId, data, controllers, cancellationToken).ConfigureAwait(false); + await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false); } } @@ -1065,13 +1064,13 @@ namespace Emby.Server.Implementations.Session { IEnumerable<Task> GetTasks() { - var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + var messageId = Guid.NewGuid(); foreach (var session in sessions) { var controllers = session.SessionControllers; foreach (var controller in controllers) { - yield return controller.SendMessage(name, messageId, data, controllers, cancellationToken); + yield return controller.SendMessage(name, messageId, data, cancellationToken); } } } @@ -1772,7 +1771,7 @@ namespace Emby.Server.Implementations.Session throw new ArgumentNullException(nameof(info)); } - var user = info.UserId.Equals(Guid.Empty) + var user = info.UserId == Guid.Empty ? null : _userManager.GetUserById(info.UserId); diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index 930f2d35d..d4e4ba1f2 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Events; -using MediaBrowser.Model.Serialization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -12,7 +11,7 @@ namespace Emby.Server.Implementations.Session /// <summary> /// Class SessionWebSocketListener /// </summary> - public class SessionWebSocketListener : IWebSocketListener, IDisposable + public sealed class SessionWebSocketListener : IWebSocketListener, IDisposable { /// <summary> /// The _session manager @@ -23,42 +22,41 @@ namespace Emby.Server.Implementations.Session /// The _logger /// </summary> private readonly ILogger _logger; - - /// <summary> - /// The _dto service - /// </summary> - private readonly IJsonSerializer _json; + private readonly ILoggerFactory _loggerFactory; private readonly IHttpServer _httpServer; - /// <summary> /// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class. /// </summary> + /// <param name="logger">The logger.</param> /// <param name="sessionManager">The session manager.</param> /// <param name="loggerFactory">The logger factory.</param> - /// <param name="json">The json.</param> /// <param name="httpServer">The HTTP server.</param> - public SessionWebSocketListener(ISessionManager sessionManager, ILoggerFactory loggerFactory, IJsonSerializer json, IHttpServer httpServer) + public SessionWebSocketListener( + ILogger<SessionWebSocketListener> logger, + ISessionManager sessionManager, + ILoggerFactory loggerFactory, + IHttpServer httpServer) { + _logger = logger; _sessionManager = sessionManager; - _logger = loggerFactory.CreateLogger(GetType().Name); - _json = json; + _loggerFactory = loggerFactory; _httpServer = httpServer; - httpServer.WebSocketConnected += _serverManager_WebSocketConnected; + + httpServer.WebSocketConnected += OnServerManagerWebSocketConnected; } - void _serverManager_WebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e) + private void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e) { - var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint); - + var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint.ToString()); if (session != null) { EnsureController(session, e.Argument); } else { - _logger.LogWarning("Unable to determine session based on url: {0}", e.Argument.Url); + _logger.LogWarning("Unable to determine session based on query string: {0}", e.Argument.QueryString); } } @@ -79,9 +77,10 @@ namespace Emby.Server.Implementations.Session return _sessionManager.GetSessionByAuthenticationToken(token, deviceId, remoteEndpoint); } + /// <inheritdoc /> public void Dispose() { - _httpServer.WebSocketConnected -= _serverManager_WebSocketConnected; + _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected; } /// <summary> @@ -94,7 +93,8 @@ namespace Emby.Server.Implementations.Session private void EnsureController(SessionInfo session, IWebSocketConnection connection) { - var controllerInfo = session.EnsureController<WebSocketController>(s => new WebSocketController(s, _logger, _sessionManager)); + var controllerInfo = session.EnsureController<WebSocketController>( + s => new WebSocketController(_loggerFactory.CreateLogger<WebSocketController>(), s, _sessionManager)); var controller = (WebSocketController)controllerInfo.Item1; controller.AddWebSocket(connection); diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs index 0d483c55f..a0274acd2 100644 --- a/Emby.Server.Implementations/Session/WebSocketController.cs +++ b/Emby.Server.Implementations/Session/WebSocketController.cs @@ -1,3 +1,7 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 +#nullable enable + using System; using System.Collections.Generic; using System.Linq; @@ -11,60 +15,63 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Session { - public class WebSocketController : ISessionController, IDisposable + public sealed class WebSocketController : ISessionController, IDisposable { - public SessionInfo Session { get; private set; } - public IReadOnlyList<IWebSocketConnection> Sockets { get; private set; } - private readonly ILogger _logger; - private readonly ISessionManager _sessionManager; + private readonly SessionInfo _session; - public WebSocketController(SessionInfo session, ILogger logger, ISessionManager sessionManager) + private readonly List<IWebSocketConnection> _sockets; + private bool _disposed = false; + + public WebSocketController( + ILogger<WebSocketController> logger, + SessionInfo session, + ISessionManager sessionManager) { - Session = session; _logger = logger; + _session = session; _sessionManager = sessionManager; - Sockets = new List<IWebSocketConnection>(); + _sockets = new List<IWebSocketConnection>(); } private bool HasOpenSockets => GetActiveSockets().Any(); + /// <inheritdoc /> public bool SupportsMediaControl => HasOpenSockets; + /// <inheritdoc /> public bool IsSessionActive => HasOpenSockets; private IEnumerable<IWebSocketConnection> GetActiveSockets() - { - return Sockets - .OrderByDescending(i => i.LastActivityDate) - .Where(i => i.State == WebSocketState.Open); - } + => _sockets.Where(i => i.State == WebSocketState.Open); public void AddWebSocket(IWebSocketConnection connection) { - var sockets = Sockets.ToList(); - sockets.Add(connection); + _logger.LogDebug("Adding websocket to session {Session}", _session.Id); + _sockets.Add(connection); - Sockets = sockets; - - connection.Closed += connection_Closed; + connection.Closed += OnConnectionClosed; } - void connection_Closed(object sender, EventArgs e) + private void OnConnectionClosed(object sender, EventArgs e) { var connection = (IWebSocketConnection)sender; - var sockets = Sockets.ToList(); - sockets.Remove(connection); - - Sockets = sockets; - - _sessionManager.CloseIfNeeded(Session); + _logger.LogDebug("Removing websocket from session {Session}", _session.Id); + _sockets.Remove(connection); + connection.Closed -= OnConnectionClosed; + _sessionManager.CloseIfNeeded(_session); } - public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken) + /// <inheritdoc /> + public Task SendMessage<T>( + string name, + Guid messageId, + T data, + CancellationToken cancellationToken) { var socket = GetActiveSockets() + .OrderByDescending(i => i.LastActivityDate) .FirstOrDefault(); if (socket == null) @@ -72,21 +79,30 @@ namespace Emby.Server.Implementations.Session return Task.CompletedTask; } - return socket.SendAsync(new WebSocketMessage<T> - { - Data = data, - MessageType = name, - MessageId = messageId - - }, cancellationToken); + return socket.SendAsync( + new WebSocketMessage<T> + { + Data = data, + MessageType = name, + MessageId = messageId + }, + cancellationToken); } + /// <inheritdoc /> public void Dispose() { - foreach (var socket in Sockets.ToList()) + if (_disposed) { - socket.Closed -= connection_Closed; + return; } + + foreach (var socket in _sockets) + { + socket.Closed -= OnConnectionClosed; + } + + _disposed = true; } } } diff --git a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs deleted file mode 100644 index 67521d6c6..000000000 --- a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.Net.WebSockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.Net; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.SocketSharp -{ - public class SharpWebSocket : IWebSocket - { - /// <summary> - /// The logger - /// </summary> - private readonly ILogger _logger; - - public event EventHandler<EventArgs> Closed; - - /// <summary> - /// Gets or sets the web socket. - /// </summary> - /// <value>The web socket.</value> - private readonly WebSocket _webSocket; - - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - private bool _disposed; - - public SharpWebSocket(WebSocket socket, ILogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _webSocket = socket ?? throw new ArgumentNullException(nameof(socket)); - } - - /// <summary> - /// Gets the state. - /// </summary> - /// <value>The state.</value> - public WebSocketState State => _webSocket.State; - - /// <summary> - /// Sends the async. - /// </summary> - /// <param name="bytes">The bytes.</param> - /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken) - { - return _webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Binary, endOfMessage, cancellationToken); - } - - /// <summary> - /// Sends the asynchronous. - /// </summary> - /// <param name="text">The text.</param> - /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken) - { - return _webSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(text)), WebSocketMessageType.Text, endOfMessage, cancellationToken); - } - - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() - { - 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 (_disposed) - { - return; - } - - if (dispose) - { - _cancellationTokenSource.Cancel(); - if (_webSocket.State == WebSocketState.Open) - { - _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client", - CancellationToken.None); - } - Closed?.Invoke(this, EventArgs.Empty); - } - - _disposed = true; - } - - /// <summary> - /// Gets or sets the receive action. - /// </summary> - /// <value>The receive action.</value> - public Action<byte[]> OnReceiveBytes { get; set; } - } -} diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs deleted file mode 100644 index b85750c9b..000000000 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.WebSockets; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; -using Emby.Server.Implementations.Net; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.SocketSharp -{ - public class WebSocketSharpListener : IHttpListener - { - private readonly ILogger _logger; - - private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); - private CancellationToken _disposeCancellationToken; - - public WebSocketSharpListener(ILogger<WebSocketSharpListener> logger) - { - _logger = logger; - _disposeCancellationToken = _disposeCancellationTokenSource.Token; - } - - public Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; } - - public Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; } - - public Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; } - - private static void LogRequest(ILogger logger, HttpRequest request) - { - var url = request.GetDisplayUrl(); - - logger.LogInformation("WS {Url}. UserAgent: {UserAgent}", url, request.Headers[HeaderNames.UserAgent].ToString()); - } - - public async Task ProcessWebSocketRequest(HttpContext ctx) - { - try - { - LogRequest(_logger, ctx.Request); - var endpoint = ctx.Connection.RemoteIpAddress.ToString(); - var url = ctx.Request.GetDisplayUrl(); - - var webSocketContext = await ctx.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false); - var socket = new SharpWebSocket(webSocketContext, _logger); - - WebSocketConnected(new WebSocketConnectEventArgs - { - Url = url, - QueryString = ctx.Request.Query, - WebSocket = socket, - Endpoint = endpoint - }); - - WebSocketReceiveResult result; - var message = new List<byte>(); - - do - { - var buffer = WebSocket.CreateServerBuffer(4096); - result = await webSocketContext.ReceiveAsync(buffer, _disposeCancellationToken); - message.AddRange(buffer.Array.Take(result.Count)); - - if (result.EndOfMessage) - { - socket.OnReceiveBytes(message.ToArray()); - message.Clear(); - } - } while (socket.State == WebSocketState.Open && result.MessageType != WebSocketMessageType.Close); - - - if (webSocketContext.State == WebSocketState.Open) - { - await webSocketContext.CloseAsync( - result.CloseStatus ?? WebSocketCloseStatus.NormalClosure, - result.CloseStatusDescription, - _disposeCancellationToken).ConfigureAwait(false); - } - - socket.Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "AcceptWebSocketAsync error"); - if (!ctx.Response.HasStarted) - { - ctx.Response.StatusCode = 500; - } - } - } - - public Task Stop() - { - _disposeCancellationTokenSource.Cancel(); - return Task.CompletedTask; - } - - /// <summary> - /// Releases the unmanaged resources and disposes of the managed resources used. - /// </summary> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private bool _disposed; - - /// <summary> - /// Releases the unmanaged resources and disposes of the managed resources used. - /// </summary> - /// <param name="disposing">Whether or not the managed resources should be disposed.</param> - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - Stop().GetAwaiter().GetResult(); - } - - _disposed = true; - } - } -} diff --git a/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs b/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs deleted file mode 100644 index eb1877440..000000000 --- a/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Model.Net; - -namespace Emby.Server.Implementations.WebSockets -{ - public interface IWebSocketHandler - { - Task ProcessMessage(WebSocketMessage<object> message, TaskCompletionSource<bool> taskCompletionSource); - } -} diff --git a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs b/Emby.Server.Implementations/WebSockets/WebSocketManager.cs deleted file mode 100644 index 31a7468fb..000000000 --- a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.WebSockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; -using UtfUnknown; - -namespace Emby.Server.Implementations.WebSockets -{ - public class WebSocketManager - { - private readonly IWebSocketHandler[] _webSocketHandlers; - private readonly IJsonSerializer _jsonSerializer; - private readonly ILogger<WebSocketManager> _logger; - private const int BufferSize = 4096; - - public WebSocketManager(IWebSocketHandler[] webSocketHandlers, IJsonSerializer jsonSerializer, ILogger<WebSocketManager> logger) - { - _webSocketHandlers = webSocketHandlers; - _jsonSerializer = jsonSerializer; - _logger = logger; - } - - public async Task OnWebSocketConnected(WebSocket webSocket) - { - var taskCompletionSource = new TaskCompletionSource<bool>(); - var cancellationToken = new CancellationTokenSource().Token; - WebSocketReceiveResult result; - var message = new List<byte>(); - - // Keep listening for incoming messages, otherwise the socket closes automatically - do - { - var buffer = WebSocket.CreateServerBuffer(BufferSize); - result = await webSocket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false); - message.AddRange(buffer.Array.Take(result.Count)); - - if (result.EndOfMessage) - { - await ProcessMessage(message.ToArray(), taskCompletionSource).ConfigureAwait(false); - message.Clear(); - } - } while (!taskCompletionSource.Task.IsCompleted && - webSocket.State == WebSocketState.Open && - result.MessageType != WebSocketMessageType.Close); - - if (webSocket.State == WebSocketState.Open) - { - await webSocket.CloseAsync( - result.CloseStatus ?? WebSocketCloseStatus.NormalClosure, - result.CloseStatusDescription, - cancellationToken).ConfigureAwait(false); - } - } - - private async Task ProcessMessage(byte[] messageBytes, TaskCompletionSource<bool> taskCompletionSource) - { - var charset = CharsetDetector.DetectFromBytes(messageBytes).Detected?.EncodingName; - var message = string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase) - ? Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length) - : Encoding.ASCII.GetString(messageBytes, 0, messageBytes.Length); - - // All messages are expected to be valid JSON objects - if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase)) - { - _logger.LogDebug("Received web socket message that is not a json structure: {Message}", message); - return; - } - - try - { - var info = _jsonSerializer.DeserializeFromString<WebSocketMessage<object>>(message); - - _logger.LogDebug("Websocket message received: {0}", info.MessageType); - - var tasks = _webSocketHandlers.Select(handler => Task.Run(() => - { - try - { - handler.ProcessMessage(info, taskCompletionSource).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "{HandlerType} failed processing WebSocket message {MessageType}", - handler.GetType().Name, info.MessageType ?? string.Empty); - } - })); - - await Task.WhenAll(tasks); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error processing web socket message"); - } - } - } -} |
