aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations')
-rw-r--r--Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs590
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs200
-rw-r--r--Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs2
-rw-r--r--Emby.Server.Implementations/ConfigurationOptions.cs2
-rw-r--r--Emby.Server.Implementations/Devices/DeviceManager.cs2
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj12
-rw-r--r--Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs2
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs2
-rw-r--r--Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs9
-rw-r--r--Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs210
-rw-r--r--Emby.Server.Implementations/HttpServer/FileWriter.cs250
-rw-r--r--Emby.Server.Implementations/HttpServer/HttpListenerHost.cs766
-rw-r--r--Emby.Server.Implementations/HttpServer/HttpResultFactory.cs721
-rw-r--r--Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs212
-rw-r--r--Emby.Server.Implementations/HttpServer/ResponseFilter.cs113
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthService.cs213
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs24
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/SessionContext.cs20
-rw-r--r--Emby.Server.Implementations/HttpServer/StreamWriter.cs120
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs8
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketManager.cs102
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_DO.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/id.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/th.json164
-rw-r--r--Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs285
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/TaskManager.cs2
-rw-r--r--Emby.Server.Implementations/Services/HttpResult.cs64
-rw-r--r--Emby.Server.Implementations/Services/RequestHelper.cs51
-rw-r--r--Emby.Server.Implementations/Services/ResponseHelper.cs141
-rw-r--r--Emby.Server.Implementations/Services/ServiceController.cs202
-rw-r--r--Emby.Server.Implementations/Services/ServiceExec.cs230
-rw-r--r--Emby.Server.Implementations/Services/ServiceHandler.cs212
-rw-r--r--Emby.Server.Implementations/Services/ServiceMethod.cs20
-rw-r--r--Emby.Server.Implementations/Services/ServicePath.cs550
-rw-r--r--Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs118
-rw-r--r--Emby.Server.Implementations/Services/UrlExtensions.cs27
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs135
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs14
-rw-r--r--Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs248
44 files changed, 737 insertions, 5323 deletions
diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
deleted file mode 100644
index 84bec9201..000000000
--- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
+++ /dev/null
@@ -1,590 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Common.Plugins;
-using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Controller.Subtitles;
-using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Notifications;
-using MediaBrowser.Model.Tasks;
-using MediaBrowser.Model.Updates;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Activity
-{
- /// <summary>
- /// Entry point for the activity logger.
- /// </summary>
- public sealed class ActivityLogEntryPoint : IServerEntryPoint
- {
- private readonly ILogger<ActivityLogEntryPoint> _logger;
- private readonly IInstallationManager _installationManager;
- private readonly ISessionManager _sessionManager;
- private readonly ITaskManager _taskManager;
- private readonly IActivityManager _activityManager;
- private readonly ILocalizationManager _localization;
- private readonly ISubtitleManager _subManager;
- private readonly IUserManager _userManager;
-
- /// <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="taskManager">The task manager.</param>
- /// <param name="activityManager">The activity manager.</param>
- /// <param name="localization">The localization manager.</param>
- /// <param name="installationManager">The installation manager.</param>
- /// <param name="subManager">The subtitle manager.</param>
- /// <param name="userManager">The user manager.</param>
- public ActivityLogEntryPoint(
- ILogger<ActivityLogEntryPoint> logger,
- ISessionManager sessionManager,
- ITaskManager taskManager,
- IActivityManager activityManager,
- ILocalizationManager localization,
- IInstallationManager installationManager,
- ISubtitleManager subManager,
- IUserManager userManager)
- {
- _logger = logger;
- _sessionManager = sessionManager;
- _taskManager = taskManager;
- _activityManager = activityManager;
- _localization = localization;
- _installationManager = installationManager;
- _subManager = subManager;
- _userManager = userManager;
- }
-
- /// <inheritdoc />
- public Task RunAsync()
- {
- _taskManager.TaskCompleted += OnTaskCompleted;
-
- _installationManager.PluginInstalled += OnPluginInstalled;
- _installationManager.PluginUninstalled += OnPluginUninstalled;
- _installationManager.PluginUpdated += OnPluginUpdated;
- _installationManager.PackageInstallationFailed += OnPackageInstallationFailed;
-
- _sessionManager.SessionStarted += OnSessionStarted;
- _sessionManager.AuthenticationFailed += OnAuthenticationFailed;
- _sessionManager.AuthenticationSucceeded += OnAuthenticationSucceeded;
- _sessionManager.SessionEnded += OnSessionEnded;
- _sessionManager.PlaybackStart += OnPlaybackStart;
- _sessionManager.PlaybackStopped += OnPlaybackStopped;
-
- _subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure;
-
- _userManager.OnUserCreated += OnUserCreated;
- _userManager.OnUserPasswordChanged += OnUserPasswordChanged;
- _userManager.OnUserDeleted += OnUserDeleted;
- _userManager.OnUserLockedOut += OnUserLockedOut;
-
- return Task.CompletedTask;
- }
-
- 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)
- {
- LogSeverity = LogLevel.Error
- }).ConfigureAwait(false);
- }
-
- private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
- {
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
- e.Provider,
- Notifications.NotificationEntryPoint.GetItemName(e.Item)),
- "SubtitleDownloadFailure",
- Guid.Empty)
- {
- ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
- ShortOverview = e.Exception.Message
- }).ConfigureAwait(false);
- }
-
- private async void OnPlaybackStopped(object sender, PlaybackStopEventArgs e)
- {
- var item = e.MediaInfo;
-
- if (item == null)
- {
- _logger.LogWarning("PlaybackStopped reported with null media info.");
- return;
- }
-
- if (e.Item != null && e.Item.IsThemeMedia)
- {
- // Don't report theme song or local trailer playback
- return;
- }
-
- if (e.Users.Count == 0)
- {
- return;
- }
-
- var user = e.Users[0];
-
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"),
- user.Username,
- GetItemName(item),
- e.DeviceName),
- GetPlaybackStoppedNotificationType(item.MediaType),
- user.Id))
- .ConfigureAwait(false);
- }
-
- private async void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
- {
- var item = e.MediaInfo;
-
- if (item == null)
- {
- _logger.LogWarning("PlaybackStart reported with null media info.");
- return;
- }
-
- if (e.Item != null && e.Item.IsThemeMedia)
- {
- // Don't report theme song or local trailer playback
- return;
- }
-
- if (e.Users.Count == 0)
- {
- return;
- }
-
- var user = e.Users.First();
-
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("UserStartedPlayingItemWithValues"),
- user.Username,
- GetItemName(item),
- e.DeviceName),
- GetPlaybackNotificationType(item.MediaType),
- user.Id))
- .ConfigureAwait(false);
- }
-
- private static string GetItemName(BaseItemDto item)
- {
- var name = item.Name;
-
- if (!string.IsNullOrEmpty(item.SeriesName))
- {
- name = item.SeriesName + " - " + name;
- }
-
- if (item.Artists != null && item.Artists.Count > 0)
- {
- name = item.Artists[0] + " - " + name;
- }
-
- return name;
- }
-
- private static string GetPlaybackNotificationType(string mediaType)
- {
- if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
- {
- return NotificationType.AudioPlayback.ToString();
- }
-
- if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
- {
- return NotificationType.VideoPlayback.ToString();
- }
-
- return null;
- }
-
- private static string GetPlaybackStoppedNotificationType(string mediaType)
- {
- if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
- {
- return NotificationType.AudioPlaybackStopped.ToString();
- }
-
- if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
- {
- return NotificationType.VideoPlaybackStopped.ToString();
- }
-
- return null;
- }
-
- private async void OnSessionEnded(object sender, SessionEventArgs e)
- {
- var session = e.SessionInfo;
-
- if (string.IsNullOrEmpty(session.UserName))
- {
- return;
- }
-
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("UserOfflineFromDevice"),
- session.UserName,
- session.DeviceName),
- "SessionEnded",
- session.UserId)
- {
- ShortOverview = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("LabelIpAddressValue"),
- session.RemoteEndPoint),
- }).ConfigureAwait(false);
- }
-
- private async void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e)
- {
- var user = e.Argument.User;
-
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("AuthenticationSucceededWithUserName"),
- user.Name),
- "AuthenticationSucceeded",
- user.Id)
- {
- ShortOverview = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("LabelIpAddressValue"),
- e.Argument.SessionInfo.RemoteEndPoint),
- }).ConfigureAwait(false);
- }
-
- private async void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e)
- {
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("FailedLoginAttemptWithUserName"),
- e.Argument.Username),
- "AuthenticationFailed",
- Guid.Empty)
- {
- LogSeverity = LogLevel.Error,
- ShortOverview = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("LabelIpAddressValue"),
- e.Argument.RemoteEndPoint),
- }).ConfigureAwait(false);
- }
-
- private async void OnUserDeleted(object sender, GenericEventArgs<User> e)
- {
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("UserDeletedWithName"),
- e.Argument.Username),
- "UserDeleted",
- Guid.Empty))
- .ConfigureAwait(false);
- }
-
- private async void OnUserPasswordChanged(object sender, GenericEventArgs<User> e)
- {
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("UserPasswordChangedWithName"),
- e.Argument.Username),
- "UserPasswordChanged",
- e.Argument.Id))
- .ConfigureAwait(false);
- }
-
- private async void OnUserCreated(object sender, GenericEventArgs<User> e)
- {
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("UserCreatedWithName"),
- e.Argument.Username),
- "UserCreated",
- e.Argument.Id))
- .ConfigureAwait(false);
- }
-
- private async void OnSessionStarted(object sender, SessionEventArgs e)
- {
- var session = e.SessionInfo;
-
- if (string.IsNullOrEmpty(session.UserName))
- {
- return;
- }
-
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("UserOnlineFromDevice"),
- session.UserName,
- session.DeviceName),
- "SessionStarted",
- session.UserId)
- {
- ShortOverview = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("LabelIpAddressValue"),
- session.RemoteEndPoint)
- }).ConfigureAwait(false);
- }
-
- private async void OnPluginUpdated(object sender, InstallationInfo e)
- {
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("PluginUpdatedWithName"),
- e.Name),
- NotificationType.PluginUpdateInstalled.ToString(),
- Guid.Empty)
- {
- ShortOverview = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("VersionNumber"),
- e.Version),
- Overview = e.Changelog
- }).ConfigureAwait(false);
- }
-
- private async void OnPluginUninstalled(object sender, IPlugin e)
- {
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("PluginUninstalledWithName"),
- e.Name),
- NotificationType.PluginUninstalled.ToString(),
- Guid.Empty))
- .ConfigureAwait(false);
- }
-
- private async void OnPluginInstalled(object sender, InstallationInfo e)
- {
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("PluginInstalledWithName"),
- e.Name),
- NotificationType.PluginInstalled.ToString(),
- Guid.Empty)
- {
- ShortOverview = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("VersionNumber"),
- e.Version)
- }).ConfigureAwait(false);
- }
-
- private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
- {
- var installationInfo = e.InstallationInfo;
-
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("NameInstallFailed"),
- installationInfo.Name),
- NotificationType.InstallationFailed.ToString(),
- Guid.Empty)
- {
- ShortOverview = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("VersionNumber"),
- installationInfo.Version),
- Overview = e.Exception.Message
- }).ConfigureAwait(false);
- }
-
- private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
- {
- var result = e.Result;
- var task = e.Task;
-
- if (task.ScheduledTask is IConfigurableScheduledTask activityTask
- && !activityTask.IsLogged)
- {
- return;
- }
-
- var time = result.EndTimeUtc - result.StartTimeUtc;
- var runningTime = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("LabelRunningTimeValue"),
- ToUserFriendlyString(time));
-
- if (result.Status == TaskCompletionStatus.Failed)
- {
- var vals = new List<string>();
-
- if (!string.IsNullOrEmpty(e.Result.ErrorMessage))
- {
- vals.Add(e.Result.ErrorMessage);
- }
-
- if (!string.IsNullOrEmpty(e.Result.LongErrorMessage))
- {
- vals.Add(e.Result.LongErrorMessage);
- }
-
- await CreateLogEntry(new ActivityLog(
- string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name),
- NotificationType.TaskFailed.ToString(),
- Guid.Empty)
- {
- LogSeverity = LogLevel.Error,
- Overview = string.Join(Environment.NewLine, vals),
- ShortOverview = runningTime
- }).ConfigureAwait(false);
- }
- }
-
- private async Task CreateLogEntry(ActivityLog entry)
- => await _activityManager.CreateAsync(entry).ConfigureAwait(false);
-
- /// <inheritdoc />
- public void Dispose()
- {
- _taskManager.TaskCompleted -= OnTaskCompleted;
-
- _installationManager.PluginInstalled -= OnPluginInstalled;
- _installationManager.PluginUninstalled -= OnPluginUninstalled;
- _installationManager.PluginUpdated -= OnPluginUpdated;
- _installationManager.PackageInstallationFailed -= OnPackageInstallationFailed;
-
- _sessionManager.SessionStarted -= OnSessionStarted;
- _sessionManager.AuthenticationFailed -= OnAuthenticationFailed;
- _sessionManager.AuthenticationSucceeded -= OnAuthenticationSucceeded;
- _sessionManager.SessionEnded -= OnSessionEnded;
-
- _sessionManager.PlaybackStart -= OnPlaybackStart;
- _sessionManager.PlaybackStopped -= OnPlaybackStopped;
-
- _subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure;
-
- _userManager.OnUserCreated -= OnUserCreated;
- _userManager.OnUserPasswordChanged -= OnUserPasswordChanged;
- _userManager.OnUserDeleted -= OnUserDeleted;
- _userManager.OnUserLockedOut -= OnUserLockedOut;
- }
-
- /// <summary>
- /// Constructs a user-friendly string for this TimeSpan instance.
- /// </summary>
- private static string ToUserFriendlyString(TimeSpan span)
- {
- const int DaysInYear = 365;
- const int DaysInMonth = 30;
-
- // Get each non-zero value from TimeSpan component
- var values = new List<string>();
-
- // Number of years
- int days = span.Days;
- if (days >= DaysInYear)
- {
- int years = days / DaysInYear;
- values.Add(CreateValueString(years, "year"));
- days %= DaysInYear;
- }
-
- // Number of months
- if (days >= DaysInMonth)
- {
- int months = days / DaysInMonth;
- values.Add(CreateValueString(months, "month"));
- days = days % DaysInMonth;
- }
-
- // Number of days
- if (days >= 1)
- {
- values.Add(CreateValueString(days, "day"));
- }
-
- // Number of hours
- if (span.Hours >= 1)
- {
- values.Add(CreateValueString(span.Hours, "hour"));
- }
-
- // Number of minutes
- if (span.Minutes >= 1)
- {
- values.Add(CreateValueString(span.Minutes, "minute"));
- }
-
- // Number of seconds (include when 0 if no other components included)
- if (span.Seconds >= 1 || values.Count == 0)
- {
- values.Add(CreateValueString(span.Seconds, "second"));
- }
-
- // Combine values into string
- var builder = new StringBuilder();
- for (int i = 0; i < values.Count; i++)
- {
- if (builder.Length > 0)
- {
- builder.Append(i == values.Count - 1 ? " and " : ", ");
- }
-
- builder.Append(values[i]);
- }
-
- // Return result
- return builder.ToString();
- }
-
- /// <summary>
- /// Constructs a string description of a time-span value.
- /// </summary>
- /// <param name="value">The value of this item.</param>
- /// <param name="description">The name of this item (singular form).</param>
- private static string CreateValueString(int value, string description)
- {
- return string.Format(
- CultureInfo.InvariantCulture,
- "{0:#,##0} {1}",
- value,
- value == 1 ? description : string.Format(CultureInfo.InvariantCulture, "{0}s", description));
- }
- }
-}
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 99530bfbd..8e9a581ea 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -37,10 +37,10 @@ using Emby.Server.Implementations.LiveTv;
using Emby.Server.Implementations.Localization;
using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Playlists;
+using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization;
-using Emby.Server.Implementations.Services;
using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
@@ -53,7 +53,6 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller;
-using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Collections;
@@ -72,6 +71,7 @@ using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
@@ -89,7 +89,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Chapters;
@@ -97,11 +96,12 @@ using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb;
using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
-using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
+using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
namespace Emby.Server.Implementations
{
@@ -122,14 +122,18 @@ namespace Emby.Server.Implementations
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
- private IHttpServer _httpServer;
+ private IWebSocketManager _webSocketManager;
private IHttpClient _httpClient;
+ private string[] _urlPrefixes;
+
/// <summary>
/// Gets a value indicating whether this instance can self restart.
/// </summary>
public bool CanSelfRestart => _startupOptions.RestartPath != null;
+ public bool CoreStartupHasCompleted { get; private set; }
+
public virtual bool CanLaunchWebBrowser
{
get
@@ -173,6 +177,8 @@ namespace Emby.Server.Implementations
/// </summary>
protected ILogger<ApplicationHost> Logger { get; }
+ protected IServiceCollection ServiceCollection { get; }
+
private IPlugin[] _plugins;
/// <summary>
@@ -238,9 +244,11 @@ namespace Emby.Server.Implementations
ILoggerFactory loggerFactory,
IStartupOptions options,
IFileSystem fileSystem,
- INetworkManager networkManager)
+ INetworkManager networkManager,
+ IServiceCollection serviceCollection)
{
_xmlSerializer = new MyXmlSerializer();
+ ServiceCollection = serviceCollection;
_networkManager = networkManager;
networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets;
@@ -440,8 +448,7 @@ namespace Emby.Server.Implementations
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
Logger.LogInformation("Core startup complete");
- _httpServer.GlobalResponse = null;
-
+ CoreStartupHasCompleted = true;
stopWatch.Restart();
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
@@ -464,7 +471,7 @@ namespace Emby.Server.Implementations
}
/// <inheritdoc/>
- public void Init(IServiceCollection serviceCollection)
+ public void Init()
{
HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber;
HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber;
@@ -493,148 +500,143 @@ namespace Emby.Server.Implementations
DiscoverTypes();
- RegisterServices(serviceCollection);
+ RegisterServices();
}
- 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.
/// </summary>
- protected virtual void RegisterServices(IServiceCollection serviceCollection)
+ protected virtual void RegisterServices()
{
- serviceCollection.AddSingleton(_startupOptions);
-
- serviceCollection.AddMemoryCache();
+ ServiceCollection.AddSingleton(_startupOptions);
- serviceCollection.AddSingleton(ConfigurationManager);
- serviceCollection.AddSingleton<IApplicationHost>(this);
+ ServiceCollection.AddMemoryCache();
- serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
+ ServiceCollection.AddSingleton(ConfigurationManager);
+ ServiceCollection.AddSingleton<IApplicationHost>(this);
- serviceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
+ ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
- serviceCollection.AddSingleton(_fileSystemManager);
- serviceCollection.AddSingleton<TvdbClientManager>();
+ ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
- serviceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>();
+ ServiceCollection.AddSingleton(_fileSystemManager);
+ ServiceCollection.AddSingleton<TvdbClientManager>();
- serviceCollection.AddSingleton(_networkManager);
+ ServiceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>();
- serviceCollection.AddSingleton<IIsoManager, IsoManager>();
+ ServiceCollection.AddSingleton(_networkManager);
- serviceCollection.AddSingleton<ITaskManager, TaskManager>();
+ ServiceCollection.AddSingleton<IIsoManager, IsoManager>();
- serviceCollection.AddSingleton(_xmlSerializer);
+ ServiceCollection.AddSingleton<ITaskManager, TaskManager>();
- serviceCollection.AddSingleton<IStreamHelper, StreamHelper>();
+ ServiceCollection.AddSingleton(_xmlSerializer);
- serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
+ ServiceCollection.AddSingleton<IStreamHelper, StreamHelper>();
- serviceCollection.AddSingleton<ISocketFactory, SocketFactory>();
+ ServiceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
- serviceCollection.AddSingleton<IInstallationManager, InstallationManager>();
+ ServiceCollection.AddSingleton<ISocketFactory, SocketFactory>();
- serviceCollection.AddSingleton<IZipClient, ZipClient>();
+ ServiceCollection.AddSingleton<IInstallationManager, InstallationManager>();
- serviceCollection.AddSingleton<IHttpResultFactory, HttpResultFactory>();
+ ServiceCollection.AddSingleton<IZipClient, ZipClient>();
- serviceCollection.AddSingleton<IServerApplicationHost>(this);
- serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
+ ServiceCollection.AddSingleton<IServerApplicationHost>(this);
+ ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
- serviceCollection.AddSingleton(ServerConfigurationManager);
+ ServiceCollection.AddSingleton(ServerConfigurationManager);
- serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
+ ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
- serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
+ ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
- serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
- serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
+ ServiceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
+ ServiceCollection.AddSingleton<IUserDataManager, UserDataManager>();
- serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
+ ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
- serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
+ ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
- serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
+ ServiceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
- serviceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
- serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
+ ServiceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
+ ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
- serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
- serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
- serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
- serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
+ ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
+ ServiceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
+ ServiceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
+ ServiceCollection.AddSingleton<ILibraryManager, LibraryManager>();
- serviceCollection.AddSingleton<IMusicManager, MusicManager>();
+ ServiceCollection.AddSingleton<IMusicManager, MusicManager>();
- serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
+ ServiceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
- serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
+ ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
- serviceCollection.AddSingleton<ServiceController>();
- serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
+ ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
- serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
+ ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
- serviceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
+ ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
- serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
+ ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>();
- serviceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
+ ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
- serviceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
+ ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
- serviceCollection.AddSingleton<IProviderManager, ProviderManager>();
+ ServiceCollection.AddSingleton<IProviderManager, ProviderManager>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
- serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
- serviceCollection.AddSingleton<IDtoService, DtoService>();
+ ServiceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
+ ServiceCollection.AddSingleton<IDtoService, DtoService>();
- serviceCollection.AddSingleton<IChannelManager, ChannelManager>();
+ ServiceCollection.AddSingleton<IChannelManager, ChannelManager>();
- serviceCollection.AddSingleton<ISessionManager, SessionManager>();
+ ServiceCollection.AddSingleton<ISessionManager, SessionManager>();
- serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
+ ServiceCollection.AddSingleton<IDlnaManager, DlnaManager>();
- serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
+ ServiceCollection.AddSingleton<ICollectionManager, CollectionManager>();
- serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
+ ServiceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
- serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
+ ServiceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
- serviceCollection.AddSingleton<LiveTvDtoService>();
- serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
+ ServiceCollection.AddSingleton<LiveTvDtoService>();
+ ServiceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
- serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
+ ServiceCollection.AddSingleton<IUserViewManager, UserViewManager>();
- serviceCollection.AddSingleton<INotificationManager, NotificationManager>();
+ ServiceCollection.AddSingleton<INotificationManager, NotificationManager>();
- serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
+ ServiceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
- serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
+ ServiceCollection.AddSingleton<IChapterManager, ChapterManager>();
- serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
+ ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
- serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
- serviceCollection.AddSingleton<ISessionContext, SessionContext>();
+ ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
+ ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
- serviceCollection.AddSingleton<IAuthService, AuthService>();
+ ServiceCollection.AddSingleton<IAuthService, AuthService>();
+ ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
- serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
+ ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
- serviceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>();
- serviceCollection.AddSingleton<EncodingHelper>();
+ ServiceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>();
+ ServiceCollection.AddSingleton<EncodingHelper>();
- serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
+ ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
- serviceCollection.AddSingleton<TranscodingJobHelper>();
- serviceCollection.AddScoped<MediaInfoHelper>();
- serviceCollection.AddScoped<AudioHelper>();
- serviceCollection.AddScoped<DynamicHlsHelper>();
+ ServiceCollection.AddSingleton<TranscodingJobHelper>();
+ ServiceCollection.AddScoped<MediaInfoHelper>();
+ ServiceCollection.AddScoped<AudioHelper>();
+ ServiceCollection.AddScoped<DynamicHlsHelper>();
}
/// <summary>
@@ -648,7 +650,7 @@ namespace Emby.Server.Implementations
_mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>();
- _httpServer = Resolve<IHttpServer>();
+ _webSocketManager = Resolve<IWebSocketManager>();
_httpClient = Resolve<IHttpClient>();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
@@ -750,7 +752,6 @@ namespace Emby.Server.Implementations
CollectionFolder.XmlSerializer = _xmlSerializer;
CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
CollectionFolder.ApplicationHost = this;
- AuthenticatedAttribute.AuthService = Resolve<IAuthService>();
}
/// <summary>
@@ -770,7 +771,8 @@ namespace Emby.Server.Implementations
.Where(i => i != null)
.ToArray();
- _httpServer.Init(GetExportTypes<IService>(), GetExports<IWebSocketListener>(), GetUrlPrefixes());
+ _urlPrefixes = GetUrlPrefixes().ToArray();
+ _webSocketManager.Init(GetExports<IWebSocketListener>());
Resolve<ILibraryManager>().AddParts(
GetExports<IResolverIgnoreRule>(),
@@ -834,6 +836,8 @@ namespace Emby.Server.Implementations
{
hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
}
+
+ plugin.RegisterServices(ServiceCollection);
}
catch (Exception ex)
{
@@ -934,7 +938,7 @@ namespace Emby.Server.Implementations
}
}
- if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
+ if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
{
requiresRestart = true;
}
@@ -1388,6 +1392,20 @@ namespace Emby.Server.Implementations
_plugins = list.ToArray();
}
+ public IEnumerable<Assembly> GetApiPluginAssemblies()
+ {
+ var assemblies = _allConcreteTypes
+ .Where(i => typeof(ControllerBase).IsAssignableFrom(i))
+ .Select(i => i.Assembly)
+ .Distinct();
+
+ foreach (var assembly in assemblies)
+ {
+ Logger.LogDebug("Found API endpoints in plugin {name}", assembly.FullName);
+ yield return assembly;
+ }
+ }
+
public virtual void LaunchUrl(string url)
{
if (!CanLaunchWebBrowser)
diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
index a15295fca..f05a30a89 100644
--- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
+++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
@@ -2,11 +2,11 @@ using System;
using System.Globalization;
using System.IO;
using Emby.Server.Implementations.AppBase;
+using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Events;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs
index 64ccff53b..fde6fa115 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -15,7 +15,7 @@ namespace Emby.Server.Implementations
public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string>
{
{ HostWebClientKey, bool.TrueString },
- { HttpListenerHost.DefaultRedirectKey, "web/index.html" },
+ { DefaultRedirectKey, "web/index.html" },
{ FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.TrueString },
diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs
index cc4b407f5..f98c694c4 100644
--- a/Emby.Server.Implementations/Devices/DeviceManager.cs
+++ b/Emby.Server.Implementations/Devices/DeviceManager.cs
@@ -7,13 +7,13 @@ using System.IO;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Devices;
-using MediaBrowser.Model.Events;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Session;
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 60564f700..0a348f0d0 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -22,7 +22,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="IPNetwork2" Version="2.5.211" />
+ <PackageReference Include="IPNetwork2" Version="2.5.224" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
@@ -32,12 +32,12 @@
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" />
- <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
- <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.7" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.7" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" />
+ <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.7" />
<PackageReference Include="Mono.Nat" Version="2.0.2" />
- <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
+ <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
<PackageReference Include="sharpcompress" Version="0.26.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
index 9fce49425..2e8cc76d2 100644
--- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
+++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
@@ -7,11 +7,11 @@ using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Events;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Events;
using Microsoft.Extensions.Logging;
using Mono.Nat;
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index 1deef7f72..c9d21d963 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -7,6 +7,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Events;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -15,7 +16,6 @@ using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
index 632735910..44d2580d6 100644
--- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Plugins;
@@ -43,22 +44,22 @@ namespace Emby.Server.Implementations.EntryPoints
return Task.CompletedTask;
}
- private async void OnLiveTvManagerSeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+ private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
{
await SendMessage("SeriesTimerCreated", e.Argument).ConfigureAwait(false);
}
- private async void OnLiveTvManagerTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+ private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
{
await SendMessage("TimerCreated", e.Argument).ConfigureAwait(false);
}
- private async void OnLiveTvManagerSeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+ private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
{
await SendMessage("SeriesTimerCancelled", e.Argument).ConfigureAwait(false);
}
- private async void OnLiveTvManagerTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+ private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
{
await SendMessage("TimerCancelled", e.Argument).ConfigureAwait(false);
}
diff --git a/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs b/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs
deleted file mode 100644
index 826d4d8dc..000000000
--- a/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs
+++ /dev/null
@@ -1,210 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Common.Plugins;
-using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Tasks;
-using MediaBrowser.Model.Updates;
-
-namespace Emby.Server.Implementations.EntryPoints
-{
- /// <summary>
- /// Class WebSocketEvents.
- /// </summary>
- public class ServerEventNotifier : IServerEntryPoint
- {
- /// <summary>
- /// The user manager.
- /// </summary>
- private readonly IUserManager _userManager;
-
- /// <summary>
- /// The installation manager.
- /// </summary>
- private readonly IInstallationManager _installationManager;
-
- /// <summary>
- /// The kernel.
- /// </summary>
- private readonly IServerApplicationHost _appHost;
-
- /// <summary>
- /// The task manager.
- /// </summary>
- private readonly ITaskManager _taskManager;
-
- private readonly ISessionManager _sessionManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ServerEventNotifier"/> class.
- /// </summary>
- /// <param name="appHost">The application host.</param>
- /// <param name="userManager">The user manager.</param>
- /// <param name="installationManager">The installation manager.</param>
- /// <param name="taskManager">The task manager.</param>
- /// <param name="sessionManager">The session manager.</param>
- public ServerEventNotifier(
- IServerApplicationHost appHost,
- IUserManager userManager,
- IInstallationManager installationManager,
- ITaskManager taskManager,
- ISessionManager sessionManager)
- {
- _userManager = userManager;
- _installationManager = installationManager;
- _appHost = appHost;
- _taskManager = taskManager;
- _sessionManager = sessionManager;
- }
-
- /// <inheritdoc />
- public Task RunAsync()
- {
- _userManager.OnUserDeleted += OnUserDeleted;
- _userManager.OnUserUpdated += OnUserUpdated;
-
- _appHost.HasPendingRestartChanged += OnHasPendingRestartChanged;
-
- _installationManager.PluginUninstalled += OnPluginUninstalled;
- _installationManager.PackageInstalling += OnPackageInstalling;
- _installationManager.PackageInstallationCancelled += OnPackageInstallationCancelled;
- _installationManager.PackageInstallationCompleted += OnPackageInstallationCompleted;
- _installationManager.PackageInstallationFailed += OnPackageInstallationFailed;
-
- _taskManager.TaskCompleted += OnTaskCompleted;
-
- return Task.CompletedTask;
- }
-
- private async void OnPackageInstalling(object sender, InstallationInfo e)
- {
- await SendMessageToAdminSessions("PackageInstalling", e).ConfigureAwait(false);
- }
-
- private async void OnPackageInstallationCancelled(object sender, InstallationInfo e)
- {
- await SendMessageToAdminSessions("PackageInstallationCancelled", e).ConfigureAwait(false);
- }
-
- private async void OnPackageInstallationCompleted(object sender, InstallationInfo e)
- {
- await SendMessageToAdminSessions("PackageInstallationCompleted", e).ConfigureAwait(false);
- }
-
- private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
- {
- await SendMessageToAdminSessions("PackageInstallationFailed", e.InstallationInfo).ConfigureAwait(false);
- }
-
- private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
- {
- await SendMessageToAdminSessions("ScheduledTaskEnded", e.Result).ConfigureAwait(false);
- }
-
- /// <summary>
- /// Installations the manager_ plugin uninstalled.
- /// </summary>
- /// <param name="sender">The sender.</param>
- /// <param name="e">The e.</param>
- private async void OnPluginUninstalled(object sender, IPlugin e)
- {
- await SendMessageToAdminSessions("PluginUninstalled", e).ConfigureAwait(false);
- }
-
- /// <summary>
- /// Handles the HasPendingRestartChanged event of the kernel control.
- /// </summary>
- /// <param name="sender">The source of the event.</param>
- /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
- private async void OnHasPendingRestartChanged(object sender, EventArgs e)
- {
- await _sessionManager.SendRestartRequiredNotification(CancellationToken.None).ConfigureAwait(false);
- }
-
- /// <summary>
- /// Users the manager_ user updated.
- /// </summary>
- /// <param name="sender">The sender.</param>
- /// <param name="e">The e.</param>
- private async void OnUserUpdated(object sender, GenericEventArgs<User> e)
- {
- var dto = _userManager.GetUserDto(e.Argument);
-
- await SendMessageToUserSession(e.Argument, "UserUpdated", dto).ConfigureAwait(false);
- }
-
- /// <summary>
- /// Users the manager_ user deleted.
- /// </summary>
- /// <param name="sender">The sender.</param>
- /// <param name="e">The e.</param>
- private async void OnUserDeleted(object sender, GenericEventArgs<User> e)
- {
- await SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture)).ConfigureAwait(false);
- }
-
- private async Task SendMessageToAdminSessions<T>(string name, T data)
- {
- try
- {
- await _sessionManager.SendMessageToAdminSessions(name, data, CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception)
- {
- }
- }
-
- private async Task SendMessageToUserSession<T>(User user, string name, T data)
- {
- try
- {
- await _sessionManager.SendMessageToUserSessions(
- new List<Guid> { user.Id },
- name,
- data,
- CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception)
- {
- }
- }
-
- /// <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)
- {
- _userManager.OnUserDeleted -= OnUserDeleted;
- _userManager.OnUserUpdated -= OnUserUpdated;
-
- _installationManager.PluginUninstalled -= OnPluginUninstalled;
- _installationManager.PackageInstalling -= OnPackageInstalling;
- _installationManager.PackageInstallationCancelled -= OnPackageInstallationCancelled;
- _installationManager.PackageInstallationCompleted -= OnPackageInstallationCompleted;
- _installationManager.PackageInstallationFailed -= OnPackageInstallationFailed;
-
- _appHost.HasPendingRestartChanged -= OnHasPendingRestartChanged;
-
- _taskManager.TaskCompleted -= OnTaskCompleted;
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs
deleted file mode 100644
index 6fce8de44..000000000
--- a/Emby.Server.Implementations/HttpServer/FileWriter.cs
+++ /dev/null
@@ -1,250 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Runtime.InteropServices;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpServer
-{
- public class FileWriter : IHttpResult
- {
- private static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
-
- private static readonly string[] _skipLogExtensions = {
- ".js",
- ".html",
- ".css"
- };
-
- private readonly IStreamHelper _streamHelper;
- private readonly ILogger _logger;
-
- /// <summary>
- /// The _options.
- /// </summary>
- private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
-
- /// <summary>
- /// The _requested ranges.
- /// </summary>
- private List<KeyValuePair<long, long?>> _requestedRanges;
-
- public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem, IStreamHelper streamHelper)
- {
- if (string.IsNullOrEmpty(contentType))
- {
- throw new ArgumentNullException(nameof(contentType));
- }
-
- _streamHelper = streamHelper;
-
- Path = path;
- _logger = logger;
- RangeHeader = rangeHeader;
-
- Headers[HeaderNames.ContentType] = contentType;
-
- TotalContentLength = fileSystem.GetFileInfo(path).Length;
- Headers[HeaderNames.AcceptRanges] = "bytes";
-
- if (string.IsNullOrWhiteSpace(rangeHeader))
- {
- Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture);
- StatusCode = HttpStatusCode.OK;
- }
- else
- {
- StatusCode = HttpStatusCode.PartialContent;
- SetRangeValues();
- }
-
- FileShare = FileShare.Read;
- Cookies = new List<Cookie>();
- }
-
- private string RangeHeader { get; set; }
-
- private bool IsHeadRequest { get; set; }
-
- private long RangeStart { get; set; }
-
- private long RangeEnd { get; set; }
-
- private long RangeLength { get; set; }
-
- public long TotalContentLength { get; set; }
-
- public Action OnComplete { get; set; }
-
- public Action OnError { get; set; }
-
- public List<Cookie> Cookies { get; private set; }
-
- public FileShare FileShare { get; set; }
-
- /// <summary>
- /// Gets the options.
- /// </summary>
- /// <value>The options.</value>
- public IDictionary<string, string> Headers => _options;
-
- public string Path { get; set; }
-
- /// <summary>
- /// Gets the requested ranges.
- /// </summary>
- /// <value>The requested ranges.</value>
- protected List<KeyValuePair<long, long?>> RequestedRanges
- {
- get
- {
- if (_requestedRanges == null)
- {
- _requestedRanges = new List<KeyValuePair<long, long?>>();
-
- // Example: bytes=0-,32-63
- var ranges = RangeHeader.Split('=')[1].Split(',');
-
- foreach (var range in ranges)
- {
- var vals = range.Split('-');
-
- long start = 0;
- long? end = null;
-
- if (!string.IsNullOrEmpty(vals[0]))
- {
- start = long.Parse(vals[0], UsCulture);
- }
-
- if (!string.IsNullOrEmpty(vals[1]))
- {
- end = long.Parse(vals[1], UsCulture);
- }
-
- _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
- }
- }
-
- return _requestedRanges;
- }
- }
-
- public string ContentType { get; set; }
-
- public IRequest RequestContext { get; set; }
-
- public object Response { get; set; }
-
- public int Status { get; set; }
-
- public HttpStatusCode StatusCode
- {
- get => (HttpStatusCode)Status;
- set => Status = (int)value;
- }
-
- /// <summary>
- /// Sets the range values.
- /// </summary>
- private void SetRangeValues()
- {
- var requestedRange = RequestedRanges[0];
-
- // If the requested range is "0-", we can optimize by just doing a stream copy
- if (!requestedRange.Value.HasValue)
- {
- RangeEnd = TotalContentLength - 1;
- }
- else
- {
- RangeEnd = requestedRange.Value.Value;
- }
-
- RangeStart = requestedRange.Key;
- RangeLength = 1 + RangeEnd - RangeStart;
-
- // Content-Length is the length of what we're serving, not the original content
- var lengthString = RangeLength.ToString(CultureInfo.InvariantCulture);
- Headers[HeaderNames.ContentLength] = lengthString;
- var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
- Headers[HeaderNames.ContentRange] = rangeString;
-
- _logger.LogDebug("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString);
- }
-
- public async Task WriteToAsync(HttpResponse response, CancellationToken cancellationToken)
- {
- try
- {
- // Headers only
- if (IsHeadRequest)
- {
- return;
- }
-
- var path = Path;
- var offset = RangeStart;
- var count = RangeLength;
-
- if (string.IsNullOrWhiteSpace(RangeHeader) || RangeStart <= 0 && RangeEnd >= TotalContentLength - 1)
- {
- var extension = System.IO.Path.GetExtension(path);
-
- if (extension == null || !_skipLogExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
- {
- _logger.LogDebug("Transmit file {0}", path);
- }
-
- offset = 0;
- count = 0;
- }
-
- await TransmitFile(response.Body, path, offset, count, FileShare, cancellationToken).ConfigureAwait(false);
- }
- finally
- {
- OnComplete?.Invoke();
- }
- }
-
- public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShare fileShare, CancellationToken cancellationToken)
- {
- var fileOptions = FileOptions.SequentialScan;
-
- // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- fileOptions |= FileOptions.Asynchronous;
- }
-
- using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, fileOptions))
- {
- if (offset > 0)
- {
- fs.Position = offset;
- }
-
- if (count > 0)
- {
- await _streamHelper.CopyToAsync(fs, stream, count, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- await fs.CopyToAsync(stream, IODefaults.CopyToBufferSize, cancellationToken).ConfigureAwait(false);
- }
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
deleted file mode 100644
index dafdd5b7b..000000000
--- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
+++ /dev/null
@@ -1,766 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-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.Services;
-using Emby.Server.Implementations.SocketSharp;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Events;
-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
- {
- /// <summary>
- /// The key for a setting that specifies the default redirect path
- /// to use for requests where the URL base prefix is invalid or missing.
- /// </summary>
- public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
-
- private readonly ILogger<HttpListenerHost> _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 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 IHostEnvironment _hostEnvironment;
-
- private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
- private bool _disposed = false;
-
- public HttpListenerHost(
- IServerApplicationHost applicationHost,
- ILogger<HttpListenerHost> logger,
- IServerConfigurationManager config,
- IConfiguration configuration,
- INetworkManager networkManager,
- IJsonSerializer jsonSerializer,
- IXmlSerializer xmlSerializer,
- ILocalizationManager localizationManager,
- ServiceController serviceController,
- IHostEnvironment hostEnvironment,
- ILoggerFactory loggerFactory)
- {
- _appHost = applicationHost;
- _logger = logger;
- _config = config;
- _defaultRedirectPath = configuration[DefaultRedirectKey];
- _baseUrlPrefix = _config.Configuration.BaseUrl;
- _networkManager = networkManager;
- _jsonSerializer = jsonSerializer;
- _xmlSerializer = xmlSerializer;
- ServiceController = serviceController;
- _hostEnvironment = hostEnvironment;
- _loggerFactory = loggerFactory;
-
- _funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
-
- Instance = this;
- ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>();
- GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
- }
-
- public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
-
- public Action<IRequest, HttpResponse, object>[] ResponseFilters { get; set; }
-
- public static HttpListenerHost Instance { get; protected set; }
-
- public string[] UrlPrefixes { get; private set; }
-
- public string GlobalResponse { get; set; }
-
- public ServiceController ServiceController { get; }
-
- public object CreateInstance(Type type)
- {
- return _appHost.CreateInstance(type);
- }
-
- private static string NormalizeUrlPath(string path)
- {
- if (path.Length > 0 && path[0] == '/')
- {
- // If the path begins with a leading slash, just return it as-is
- return path;
- }
- else
- {
- // If the path does not begin with a leading slash, append one for consistency
- return "/" + path;
- }
- }
-
- /// <summary>
- /// Applies the request filters. Returns whether or not the request has been handled
- /// and no more processing should be done.
- /// </summary>
- /// <returns></returns>
- public void ApplyRequestFilters(IRequest req, HttpResponse res, object requestDto)
- {
- // Exec all RequestFilter attributes with Priority < 0
- var attributes = GetRequestFilterAttributes(requestDto.GetType());
-
- int count = attributes.Count;
- int i = 0;
- for (; i < count && attributes[i].Priority < 0; i++)
- {
- var attribute = attributes[i];
- attribute.RequestFilter(req, res, requestDto);
- }
-
- // Exec remaining RequestFilter attributes with Priority >= 0
- for (; i < count && attributes[i].Priority >= 0; i++)
- {
- var attribute = attributes[i];
- attribute.RequestFilter(req, res, requestDto);
- }
- }
-
- public Type GetServiceTypeByRequest(Type requestType)
- {
- _serviceOperationsMap.TryGetValue(requestType, out var serviceType);
- return serviceType;
- }
-
- public void AddServiceInfo(Type serviceType, Type requestType)
- {
- _serviceOperationsMap[requestType] = serviceType;
- }
-
- private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType)
- {
- var attributes = requestDtoType.GetCustomAttributes(true).OfType<IHasRequestFilter>().ToList();
-
- var serviceType = GetServiceTypeByRequest(requestDtoType);
- if (serviceType != null)
- {
- attributes.AddRange(serviceType.GetCustomAttributes(true).OfType<IHasRequestFilter>());
- }
-
- attributes.Sort((x, y) => x.Priority - y.Priority);
-
- return attributes;
- }
-
- private static Exception GetActualException(Exception ex)
- {
- if (ex is AggregateException agg)
- {
- var inner = agg.InnerException;
- if (inner != null)
- {
- return GetActualException(inner);
- }
- else
- {
- var inners = agg.InnerExceptions;
- if (inners.Count > 0)
- {
- return GetActualException(inners[0]);
- }
- }
- }
-
- return ex;
- }
-
- private int GetStatusCode(Exception ex)
- {
- switch (ex)
- {
- case ArgumentException _: return 400;
- case AuthenticationException _: return 401;
- case SecurityException _: return 403;
- case DirectoryNotFoundException _:
- case FileNotFoundException _:
- case ResourceNotFoundException _: return 404;
- case MethodNotAllowedException _: return 405;
- default: return 500;
- }
- }
-
- private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace)
- {
- if (ignoreStackTrace)
- {
- _logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog);
- }
- else
- {
- _logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog);
- }
-
- var httpRes = httpReq.Response;
-
- if (httpRes.HasStarted)
- {
- return;
- }
-
- httpRes.StatusCode = statusCode;
-
- var errContent = _hostEnvironment.IsDevelopment()
- ? (NormalizeExceptionMessage(ex) ?? string.Empty)
- : "Error processing request.";
- httpRes.ContentType = "text/plain";
- httpRes.ContentLength = errContent.Length;
- await httpRes.WriteAsync(errContent).ConfigureAwait(false);
- }
-
- private string NormalizeExceptionMessage(Exception ex)
- {
- // Do not expose the exception message for AuthenticationException
- if (ex is AuthenticationException)
- {
- return null;
- }
-
- // Strip any information we don't want to reveal
- return ex.Message
- ?.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase)
- .Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
- }
-
- public static string RemoveQueryStringByKey(string url, string key)
- {
- var uri = new Uri(url);
-
- // this gets all the query string key value pairs as a collection
- var newQueryString = QueryHelpers.ParseQuery(uri.Query);
-
- var originalCount = newQueryString.Count;
-
- if (originalCount == 0)
- {
- return url;
- }
-
- // this removes the key if exists
- newQueryString.Remove(key);
-
- if (originalCount == newQueryString.Count)
- {
- return url;
- }
-
- // this gets the page path from root without QueryString
- string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0];
-
- return newQueryString.Count > 0
- ? QueryHelpers.AddQueryString(pagePathWithoutQueryString, newQueryString.ToDictionary(kv => kv.Key, kv => kv.Value.ToString()))
- : pagePathWithoutQueryString;
- }
-
- private static string GetUrlToLog(string url)
- {
- url = RemoveQueryStringByKey(url, "api_key");
-
- return url;
- }
-
- private static string NormalizeConfiguredLocalAddress(string address)
- {
- var add = address.AsSpan().Trim('/');
- int index = add.IndexOf('/');
- if (index != -1)
- {
- add = add.Slice(index + 1);
- }
-
- return add.TrimStart('/').ToString();
- }
-
- private bool ValidateHost(string host)
- {
- var hosts = _config
- .Configuration
- .LocalNetworkAddresses
- .Select(NormalizeConfiguredLocalAddress)
- .ToList();
-
- if (hosts.Count == 0)
- {
- return true;
- }
-
- host ??= string.Empty;
-
- if (_networkManager.IsInPrivateAddressSpace(host))
- {
- hosts.Add("localhost");
- hosts.Add("127.0.0.1");
-
- return hosts.Any(i => host.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1);
- }
-
- return true;
- }
-
- private bool ValidateRequest(string remoteIp, bool isLocal)
- {
- if (isLocal)
- {
- return true;
- }
-
- if (_config.Configuration.EnableRemoteAccess)
- {
- var addressFilter = _config.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
-
- if (addressFilter.Length > 0 && !_networkManager.IsInLocalNetwork(remoteIp))
- {
- if (_config.Configuration.IsRemoteIPFilterBlacklist)
- {
- return !_networkManager.IsAddressInSubnets(remoteIp, addressFilter);
- }
- else
- {
- return _networkManager.IsAddressInSubnets(remoteIp, addressFilter);
- }
- }
- }
- else
- {
- if (!_networkManager.IsInLocalNetwork(remoteIp))
- {
- return false;
- }
- }
-
- 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.ListenWithHttps
- && !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase))
- {
- // 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;
- }
-
- 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);
- return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted);
- }
-
- /// <summary>
- /// Overridable method that can be used to implement a custom handler.
- /// </summary>
- private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
- {
- var stopWatch = new Stopwatch();
- stopWatch.Start();
- var httpRes = httpReq.Response;
- string urlToLog = GetUrlToLog(urlString);
- string remoteIp = httpReq.RemoteIp;
-
- try
- {
- if (_disposed)
- {
- httpRes.StatusCode = 503;
- httpRes.ContentType = "text/plain";
- await httpRes.WriteAsync("Server shutting down", cancellationToken).ConfigureAwait(false);
- return;
- }
-
- if (!ValidateHost(host))
- {
- httpRes.StatusCode = 400;
- httpRes.ContentType = "text/plain";
- await httpRes.WriteAsync("Invalid host", cancellationToken).ConfigureAwait(false);
- return;
- }
-
- if (!ValidateRequest(remoteIp, httpReq.IsLocal))
- {
- httpRes.StatusCode = 403;
- httpRes.ContentType = "text/plain";
- await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false);
- return;
- }
-
- if (!ValidateSsl(httpReq.RemoteIp, urlString))
- {
- RedirectToSecureUrl(httpReq, httpRes, urlString);
- return;
- }
-
- if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
- {
- httpRes.StatusCode = 200;
- 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;
- }
-
- if (string.Equals(localPath, _baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
- || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
- || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)
- || string.IsNullOrEmpty(localPath)
- || !localPath.StartsWith(_baseUrlPrefix, StringComparison.OrdinalIgnoreCase))
- {
- // Always redirect back to the default path if the base prefix is invalid or missing
- _logger.LogDebug("Normalizing a URL at {0}", localPath);
- httpRes.Redirect(_baseUrlPrefix + "/" + _defaultRedirectPath);
- return;
- }
-
- if (!string.IsNullOrEmpty(GlobalResponse))
- {
- // We don't want the address pings in ApplicationHost to fail
- if (localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1)
- {
- httpRes.StatusCode = 503;
- httpRes.ContentType = "text/html";
- await httpRes.WriteAsync(GlobalResponse, cancellationToken).ConfigureAwait(false);
- return;
- }
- }
-
- var handler = GetServiceHandler(httpReq);
- if (handler != null)
- {
- await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- throw new FileNotFoundException();
- }
- }
- catch (Exception requestEx)
- {
- try
- {
- var requestInnerEx = GetActualException(requestEx);
- var statusCode = GetStatusCode(requestInnerEx);
-
- foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
- {
- if (!httpRes.Headers.ContainsKey(key))
- {
- httpRes.Headers.Add(key, value);
- }
- }
-
- bool ignoreStackTrace =
- requestInnerEx is SocketException
- || requestInnerEx is IOException
- || requestInnerEx is OperationCanceledException
- || requestInnerEx is SecurityException
- || requestInnerEx is AuthenticationException
- || requestInnerEx is FileNotFoundException;
-
- // Do not handle 500 server exceptions manually when in development mode.
- // Instead, re-throw the exception so it can be handled by the DeveloperExceptionPageMiddleware.
- // However, do not use the DeveloperExceptionPageMiddleware when the stack trace should be ignored,
- // because it will log the stack trace when it handles the exception.
- if (statusCode == 500 && !ignoreStackTrace && _hostEnvironment.IsDevelopment())
- {
- throw;
- }
-
- await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false);
- }
- catch (Exception handlerException)
- {
- var aggregateEx = new AggregateException("Error while handling request exception", requestEx, handlerException);
- _logger.LogError(aggregateEx, "Error while handling exception in response to {Url}", urlToLog);
-
- if (_hostEnvironment.IsDevelopment())
- {
- throw aggregateEx;
- }
- }
- }
- finally
- {
- if (httpRes.StatusCode >= 500)
- {
- _logger.LogDebug("Sending HTTP Response 500 in response to {Url}", urlToLog);
- }
-
- stopWatch.Stop();
- var elapsed = stopWatch.Elapsed;
- if (elapsed.TotalMilliseconds > 500)
- {
- _logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog);
- }
- }
- }
-
- 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);
-
- using 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)
- {
- var pathInfo = httpReq.PathInfo;
-
- pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType);
- var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo);
- if (restPath != null)
- {
- return new ServiceHandler(restPath, contentType);
- }
-
- _logger.LogError("Could not find handler for {PathInfo}", pathInfo);
- return null;
- }
-
- private void RedirectToSecureUrl(IHttpRequest httpReq, HttpResponse httpRes, string url)
- {
- if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
- {
- var builder = new UriBuilder(uri)
- {
- Port = _config.Configuration.PublicHttpsPort,
- Scheme = "https"
- };
- url = builder.Uri.ToString();
- }
-
- httpRes.Redirect(url);
- }
-
- /// <summary>
- /// Adds the rest handlers.
- /// </summary>
- /// <param name="serviceTypes">The service types to register with the <see cref="ServiceController"/>.</param>
- /// <param name="listeners">The web socket listeners.</param>
- /// <param name="urlPrefixes">The URL prefixes. See <see cref="UrlPrefixes"/>.</param>
- public void Init(IEnumerable<Type> serviceTypes, IEnumerable<IWebSocketListener> listeners, IEnumerable<string> urlPrefixes)
- {
- _webSocketListeners = listeners.ToArray();
- UrlPrefixes = urlPrefixes.ToArray();
-
- ServiceController.Init(this, serviceTypes);
-
- ResponseFilters = new Action<IRequest, HttpResponse, object>[]
- {
- new ResponseFilter(this, _logger).FilterResponse
- };
- }
-
- public RouteAttribute[] GetRouteAttributes(Type requestType)
- {
- var routes = requestType.GetTypeInfo().GetCustomAttributes<RouteAttribute>(true).ToList();
- var clone = routes.ToList();
-
- foreach (var route in clone)
- {
- routes.Add(new RouteAttribute(NormalizeCustomRoutePath(route.Path), route.Verbs)
- {
- Notes = route.Notes,
- Priority = route.Priority,
- Summary = route.Summary
- });
-
- routes.Add(new RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs)
- {
- Notes = route.Notes,
- Priority = route.Priority,
- Summary = route.Summary
- });
-
- routes.Add(new RouteAttribute(NormalizeMediaBrowserRoutePath(route.Path), route.Verbs)
- {
- Notes = route.Notes,
- Priority = route.Priority,
- Summary = route.Summary
- });
- }
-
- return routes.ToArray();
- }
-
- public Func<string, object> GetParseFn(Type propertyType)
- {
- return _funcParseFn(propertyType);
- }
-
- public void SerializeToJson(object o, Stream stream)
- {
- _jsonSerializer.SerializeToStream(o, stream);
- }
-
- public void SerializeToXml(object o, Stream stream)
- {
- _xmlSerializer.SerializeToStream(o, stream);
- }
-
- public Task<object> DeserializeXml(Type type, Stream stream)
- {
- return Task.FromResult(_xmlSerializer.DeserializeFromStream(type, stream));
- }
-
- public Task<object> DeserializeJson(Type type, Stream stream)
- {
- return _jsonSerializer.DeserializeFromStreamAsync(stream, type);
- }
-
- private string NormalizeEmbyRoutePath(string path)
- {
- _logger.LogDebug("Normalizing /emby route");
- return _baseUrlPrefix + "/emby" + NormalizeUrlPath(path);
- }
-
- private string NormalizeMediaBrowserRoutePath(string path)
- {
- _logger.LogDebug("Normalizing /mediabrowser route");
- return _baseUrlPrefix + "/mediabrowser" + NormalizeUrlPath(path);
- }
-
- private string NormalizeCustomRoutePath(string path)
- {
- _logger.LogDebug("Normalizing custom route {0}", path);
- return _baseUrlPrefix + NormalizeUrlPath(path);
- }
-
- /// <summary>
- /// Processes the web socket message received.
- /// </summary>
- /// <param name="result">The result.</param>
- private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
- {
- if (_disposed)
- {
- return Task.CompletedTask;
- }
-
- IEnumerable<Task> GetTasks()
- {
- foreach (var x in _webSocketListeners)
- {
- yield return x.ProcessMessageAsync(result);
- }
- }
-
- return Task.WhenAll(GetTasks());
- }
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
deleted file mode 100644
index 688216373..000000000
--- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
+++ /dev/null
@@ -1,721 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.IO.Compression;
-using System.Net;
-using System.Runtime.Serialization;
-using System.Text;
-using System.Threading.Tasks;
-using System.Xml;
-using Emby.Server.Implementations.Services;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Primitives;
-using Microsoft.Net.Http.Headers;
-using IRequest = MediaBrowser.Model.Services.IRequest;
-using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
-
-namespace Emby.Server.Implementations.HttpServer
-{
- /// <summary>
- /// Class HttpResultFactory.
- /// </summary>
- public class HttpResultFactory : IHttpResultFactory
- {
- // Last-Modified and If-Modified-Since must follow strict date format,
- // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
- private const string HttpDateFormat = "ddd, dd MMM yyyy HH:mm:ss \"GMT\"";
- // We specifically use en-US culture because both day of week and month names require it
- private static readonly CultureInfo _enUSculture = new CultureInfo("en-US", false);
-
- /// <summary>
- /// The logger.
- /// </summary>
- private readonly ILogger<HttpResultFactory> _logger;
- private readonly IFileSystem _fileSystem;
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IStreamHelper _streamHelper;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="HttpResultFactory" /> class.
- /// </summary>
- public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IStreamHelper streamHelper)
- {
- _fileSystem = fileSystem;
- _jsonSerializer = jsonSerializer;
- _streamHelper = streamHelper;
- _logger = loggerfactory.CreateLogger<HttpResultFactory>();
- }
-
- /// <summary>
- /// Gets the result.
- /// </summary>
- /// <param name="requestContext">The request context.</param>
- /// <param name="content">The content.</param>
- /// <param name="contentType">Type of the content.</param>
- /// <param name="responseHeaders">The response headers.</param>
- /// <returns>System.Object.</returns>
- public object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary<string, string> responseHeaders = null)
- {
- return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
- }
-
- public object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null)
- {
- return GetHttpResult(null, content, contentType, true, responseHeaders);
- }
-
- public object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary<string, string> responseHeaders = null)
- {
- return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
- }
-
- public object GetResult(IRequest requestContext, string content, string contentType, IDictionary<string, string> responseHeaders = null)
- {
- return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
- }
-
- public object GetRedirectResult(string url)
- {
- var responseHeaders = new Dictionary<string, string>();
- responseHeaders[HeaderNames.Location] = url;
-
- var result = new HttpResult(Array.Empty<byte>(), "text/plain", HttpStatusCode.Redirect);
-
- AddResponseHeaders(result, responseHeaders);
-
- return result;
- }
-
- /// <summary>
- /// Gets the HTTP result.
- /// </summary>
- private IHasHeaders GetHttpResult(IRequest requestContext, Stream content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
- {
- var result = new StreamWriter(content, contentType);
-
- if (responseHeaders == null)
- {
- responseHeaders = new Dictionary<string, string>();
- }
-
- if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out _))
- {
- responseHeaders[HeaderNames.Expires] = "0";
- }
-
- AddResponseHeaders(result, responseHeaders);
-
- return result;
- }
-
- /// <summary>
- /// Gets the HTTP result.
- /// </summary>
- private IHasHeaders GetHttpResult(IRequest requestContext, byte[] content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
- {
- string compressionType = null;
- bool isHeadRequest = false;
-
- if (requestContext != null)
- {
- compressionType = GetCompressionType(requestContext, content, contentType);
- isHeadRequest = string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
- }
-
- IHasHeaders result;
- if (string.IsNullOrEmpty(compressionType))
- {
- var contentLength = content.Length;
-
- if (isHeadRequest)
- {
- content = Array.Empty<byte>();
- }
-
- result = new StreamWriter(content, contentType, contentLength);
- }
- else
- {
- result = GetCompressedResult(content, compressionType, responseHeaders, isHeadRequest, contentType);
- }
-
- if (responseHeaders == null)
- {
- responseHeaders = new Dictionary<string, string>();
- }
-
- if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
- {
- responseHeaders[HeaderNames.Expires] = "0";
- }
-
- AddResponseHeaders(result, responseHeaders);
-
- return result;
- }
-
- /// <summary>
- /// Gets the HTTP result.
- /// </summary>
- private IHasHeaders GetHttpResult(IRequest requestContext, string content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
- {
- IHasHeaders result;
-
- var bytes = Encoding.UTF8.GetBytes(content);
-
- var compressionType = requestContext == null ? null : GetCompressionType(requestContext, bytes, contentType);
-
- var isHeadRequest = requestContext == null ? false : string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
-
- if (string.IsNullOrEmpty(compressionType))
- {
- var contentLength = bytes.Length;
-
- if (isHeadRequest)
- {
- bytes = Array.Empty<byte>();
- }
-
- result = new StreamWriter(bytes, contentType, contentLength);
- }
- else
- {
- result = GetCompressedResult(bytes, compressionType, responseHeaders, isHeadRequest, contentType);
- }
-
- if (responseHeaders == null)
- {
- responseHeaders = new Dictionary<string, string>();
- }
-
- if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
- {
- responseHeaders[HeaderNames.Expires] = "0";
- }
-
- AddResponseHeaders(result, responseHeaders);
-
- return result;
- }
-
- /// <summary>
- /// Gets the optimized result.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- public object GetResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null)
- where T : class
- {
- if (result == null)
- {
- throw new ArgumentNullException(nameof(result));
- }
-
- if (responseHeaders == null)
- {
- responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- }
-
- responseHeaders[HeaderNames.Expires] = "0";
-
- return ToOptimizedResultInternal(requestContext, result, responseHeaders);
- }
-
- private string GetCompressionType(IRequest request, byte[] content, string responseContentType)
- {
- if (responseContentType == null)
- {
- return null;
- }
-
- // Per apple docs, hls manifests must be compressed
- if (!responseContentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) &&
- responseContentType.IndexOf("json", StringComparison.OrdinalIgnoreCase) == -1 &&
- responseContentType.IndexOf("javascript", StringComparison.OrdinalIgnoreCase) == -1 &&
- responseContentType.IndexOf("xml", StringComparison.OrdinalIgnoreCase) == -1 &&
- responseContentType.IndexOf("application/x-mpegURL", StringComparison.OrdinalIgnoreCase) == -1)
- {
- return null;
- }
-
- if (content.Length < 1024)
- {
- return null;
- }
-
- return GetCompressionType(request);
- }
-
- private static string GetCompressionType(IRequest request)
- {
- var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString();
-
- if (!string.IsNullOrEmpty(acceptEncoding))
- {
- // if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1)
- // return "br";
-
- if (acceptEncoding.Contains("deflate", StringComparison.OrdinalIgnoreCase))
- {
- return "deflate";
- }
-
- if (acceptEncoding.Contains("gzip", StringComparison.OrdinalIgnoreCase))
- {
- return "gzip";
- }
- }
-
- return null;
- }
-
- /// <summary>
- /// Returns the optimized result for the IRequestContext.
- /// Does not use or store results in any cache.
- /// </summary>
- /// <param name="request"></param>
- /// <param name="dto"></param>
- /// <returns></returns>
- public object ToOptimizedResult<T>(IRequest request, T dto)
- {
- return ToOptimizedResultInternal(request, dto);
- }
-
- private object ToOptimizedResultInternal<T>(IRequest request, T dto, IDictionary<string, string> responseHeaders = null)
- {
- // TODO: @bond use Span and .Equals
- var contentType = request.ResponseContentType?.Split(';')[0].Trim().ToLowerInvariant();
-
- switch (contentType)
- {
- case "application/xml":
- case "text/xml":
- case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
- return GetHttpResult(request, SerializeToXmlString(dto), contentType, false, responseHeaders);
-
- case "application/json":
- case "text/json":
- return GetHttpResult(request, _jsonSerializer.SerializeToString(dto), contentType, false, responseHeaders);
- default:
- break;
- }
-
- var isHeadRequest = string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase);
-
- var ms = new MemoryStream();
- var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
-
- writerFn(dto, ms);
-
- ms.Position = 0;
-
- if (isHeadRequest)
- {
- using (ms)
- {
- return GetHttpResult(request, Array.Empty<byte>(), contentType, true, responseHeaders);
- }
- }
-
- return GetHttpResult(request, ms, contentType, true, responseHeaders);
- }
-
- private IHasHeaders GetCompressedResult(
- byte[] content,
- string requestedCompressionType,
- IDictionary<string, string> responseHeaders,
- bool isHeadRequest,
- string contentType)
- {
- if (responseHeaders == null)
- {
- responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- }
-
- content = Compress(content, requestedCompressionType);
- responseHeaders[HeaderNames.ContentEncoding] = requestedCompressionType;
-
- responseHeaders[HeaderNames.Vary] = HeaderNames.AcceptEncoding;
-
- var contentLength = content.Length;
-
- if (isHeadRequest)
- {
- var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength);
- AddResponseHeaders(result, responseHeaders);
- return result;
- }
- else
- {
- var result = new StreamWriter(content, contentType, contentLength);
- AddResponseHeaders(result, responseHeaders);
- return result;
- }
- }
-
- private byte[] Compress(byte[] bytes, string compressionType)
- {
- if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase))
- {
- return Deflate(bytes);
- }
-
- if (string.Equals(compressionType, "gzip", StringComparison.OrdinalIgnoreCase))
- {
- return GZip(bytes);
- }
-
- throw new NotSupportedException(compressionType);
- }
-
- private static byte[] Deflate(byte[] bytes)
- {
- // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream
- // Which means we must use MemoryStream since you have to use ToArray() on a closed Stream
- using (var ms = new MemoryStream())
- using (var zipStream = new DeflateStream(ms, CompressionMode.Compress))
- {
- zipStream.Write(bytes, 0, bytes.Length);
- zipStream.Dispose();
-
- return ms.ToArray();
- }
- }
-
- private static byte[] GZip(byte[] buffer)
- {
- using (var ms = new MemoryStream())
- using (var zipStream = new GZipStream(ms, CompressionMode.Compress))
- {
- zipStream.Write(buffer, 0, buffer.Length);
- zipStream.Dispose();
-
- return ms.ToArray();
- }
- }
-
- private static string SerializeToXmlString(object from)
- {
- using (var ms = new MemoryStream())
- {
- var xwSettings = new XmlWriterSettings();
- xwSettings.Encoding = new UTF8Encoding(false);
- xwSettings.OmitXmlDeclaration = false;
-
- using (var xw = XmlWriter.Create(ms, xwSettings))
- {
- var serializer = new DataContractSerializer(from.GetType());
- serializer.WriteObject(xw, from);
- xw.Flush();
- ms.Seek(0, SeekOrigin.Begin);
- using (var reader = new StreamReader(ms))
- {
- return reader.ReadToEnd();
- }
- }
- }
- }
-
- /// <summary>
- /// Pres the process optimized result.
- /// </summary>
- private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, StaticResultOptions options)
- {
- bool noCache = requestContext.Headers[HeaderNames.CacheControl].ToString().IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1;
- AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified);
-
- if (!noCache)
- {
- if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var ifModifiedSinceHeader))
- {
- _logger.LogDebug("Failed to parse If-Modified-Since header date: {0}", requestContext.Headers[HeaderNames.IfModifiedSince]);
- return null;
- }
-
- if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified))
- {
- AddAgeHeader(responseHeaders, options.DateLastModified);
-
- var result = new HttpResult(Array.Empty<byte>(), options.ContentType ?? "text/html", HttpStatusCode.NotModified);
-
- AddResponseHeaders(result, responseHeaders);
-
- return result;
- }
- }
-
- return null;
- }
-
- public Task<object> GetStaticFileResult(IRequest requestContext,
- string path,
- FileShare fileShare = FileShare.Read)
- {
- if (string.IsNullOrEmpty(path))
- {
- throw new ArgumentNullException(nameof(path));
- }
-
- return GetStaticFileResult(requestContext, new StaticFileResultOptions
- {
- Path = path,
- FileShare = fileShare
- });
- }
-
- public Task<object> GetStaticFileResult(IRequest requestContext, StaticFileResultOptions options)
- {
- var path = options.Path;
- var fileShare = options.FileShare;
-
- if (string.IsNullOrEmpty(path))
- {
- throw new ArgumentException("Path can't be empty.", nameof(options));
- }
-
- if (fileShare != FileShare.Read && fileShare != FileShare.ReadWrite)
- {
- throw new ArgumentException("FileShare must be either Read or ReadWrite");
- }
-
- if (string.IsNullOrEmpty(options.ContentType))
- {
- options.ContentType = MimeTypes.GetMimeType(path);
- }
-
- if (!options.DateLastModified.HasValue)
- {
- options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path);
- }
-
- options.ContentFactory = () => Task.FromResult(GetFileStream(path, fileShare));
-
- options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
- return GetStaticResult(requestContext, options);
- }
-
- /// <summary>
- /// Gets the file stream.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="fileShare">The file share.</param>
- /// <returns>Stream.</returns>
- private Stream GetFileStream(string path, FileShare fileShare)
- {
- return new FileStream(path, FileMode.Open, FileAccess.Read, fileShare);
- }
-
- public Task<object> GetStaticResult(IRequest requestContext,
- Guid cacheKey,
- DateTime? lastDateModified,
- TimeSpan? cacheDuration,
- string contentType,
- Func<Task<Stream>> factoryFn,
- IDictionary<string, string> responseHeaders = null,
- bool isHeadRequest = false)
- {
- return GetStaticResult(requestContext, new StaticResultOptions
- {
- CacheDuration = cacheDuration,
- ContentFactory = factoryFn,
- ContentType = contentType,
- DateLastModified = lastDateModified,
- IsHeadRequest = isHeadRequest,
- ResponseHeaders = responseHeaders
- });
- }
-
- public async Task<object> GetStaticResult(IRequest requestContext, StaticResultOptions options)
- {
- options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
- var contentType = options.ContentType;
- if (!StringValues.IsNullOrEmpty(requestContext.Headers[HeaderNames.IfModifiedSince]))
- {
- // See if the result is already cached in the browser
- var result = GetCachedResult(requestContext, options.ResponseHeaders, options);
-
- if (result != null)
- {
- return result;
- }
- }
-
- // TODO: We don't really need the option value
- var isHeadRequest = options.IsHeadRequest || string.Equals(requestContext.Verb, "HEAD", StringComparison.OrdinalIgnoreCase);
- var factoryFn = options.ContentFactory;
- var responseHeaders = options.ResponseHeaders;
- AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified);
- AddAgeHeader(responseHeaders, options.DateLastModified);
-
- var rangeHeader = requestContext.Headers[HeaderNames.Range];
-
- if (!isHeadRequest && !string.IsNullOrEmpty(options.Path))
- {
- var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem, _streamHelper)
- {
- OnComplete = options.OnComplete,
- OnError = options.OnError,
- FileShare = options.FileShare
- };
-
- AddResponseHeaders(hasHeaders, options.ResponseHeaders);
- return hasHeaders;
- }
-
- var stream = await factoryFn().ConfigureAwait(false);
-
- var totalContentLength = options.ContentLength;
- if (!totalContentLength.HasValue)
- {
- try
- {
- totalContentLength = stream.Length;
- }
- catch (NotSupportedException)
- {
- }
- }
-
- if (!string.IsNullOrWhiteSpace(rangeHeader) && totalContentLength.HasValue)
- {
- var hasHeaders = new RangeRequestWriter(rangeHeader, totalContentLength.Value, stream, contentType, isHeadRequest)
- {
- OnComplete = options.OnComplete
- };
-
- AddResponseHeaders(hasHeaders, options.ResponseHeaders);
- return hasHeaders;
- }
- else
- {
- if (totalContentLength.HasValue)
- {
- responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture);
- }
-
- if (isHeadRequest)
- {
- using (stream)
- {
- return GetHttpResult(requestContext, Array.Empty<byte>(), contentType, true, responseHeaders);
- }
- }
-
- var hasHeaders = new StreamWriter(stream, contentType)
- {
- OnComplete = options.OnComplete,
- OnError = options.OnError
- };
-
- AddResponseHeaders(hasHeaders, options.ResponseHeaders);
- return hasHeaders;
- }
- }
-
- /// <summary>
- /// Adds the caching responseHeaders.
- /// </summary>
- private void AddCachingHeaders(
- IDictionary<string, string> responseHeaders,
- TimeSpan? cacheDuration,
- bool noCache,
- DateTime? lastModifiedDate)
- {
- if (noCache)
- {
- responseHeaders[HeaderNames.CacheControl] = "no-cache, no-store, must-revalidate";
- responseHeaders[HeaderNames.Pragma] = "no-cache, no-store, must-revalidate";
- return;
- }
-
- if (cacheDuration.HasValue)
- {
- responseHeaders[HeaderNames.CacheControl] = "public, max-age=" + cacheDuration.Value.TotalSeconds;
- }
- else
- {
- responseHeaders[HeaderNames.CacheControl] = "public";
- }
-
- if (lastModifiedDate.HasValue)
- {
- responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToUniversalTime().ToString(HttpDateFormat, _enUSculture);
- }
- }
-
- /// <summary>
- /// Adds the age header.
- /// </summary>
- /// <param name="responseHeaders">The responseHeaders.</param>
- /// <param name="lastDateModified">The last date modified.</param>
- private static void AddAgeHeader(IDictionary<string, string> responseHeaders, DateTime? lastDateModified)
- {
- if (lastDateModified.HasValue)
- {
- responseHeaders[HeaderNames.Age] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture);
- }
- }
-
- /// <summary>
- /// Determines whether [is not modified] [the specified if modified since].
- /// </summary>
- /// <param name="ifModifiedSince">If modified since.</param>
- /// <param name="cacheDuration">Duration of the cache.</param>
- /// <param name="dateModified">The date modified.</param>
- /// <returns><c>true</c> if [is not modified] [the specified if modified since]; otherwise, <c>false</c>.</returns>
- private bool IsNotModified(DateTime ifModifiedSince, TimeSpan? cacheDuration, DateTime? dateModified)
- {
- if (dateModified.HasValue)
- {
- var lastModified = NormalizeDateForComparison(dateModified.Value);
- ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
-
- return lastModified <= ifModifiedSince;
- }
-
- if (cacheDuration.HasValue)
- {
- var cacheExpirationDate = ifModifiedSince.Add(cacheDuration.Value);
-
- if (DateTime.UtcNow < cacheExpirationDate)
- {
- return true;
- }
- }
-
- return false;
- }
-
-
- /// <summary>
- /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that.
- /// </summary>
- /// <param name="date">The date.</param>
- /// <returns>DateTime.</returns>
- private static DateTime NormalizeDateForComparison(DateTime date)
- {
- return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind);
- }
-
- /// <summary>
- /// Adds the response headers.
- /// </summary>
- /// <param name="hasHeaders">The has options.</param>
- /// <param name="responseHeaders">The response headers.</param>
- private static void AddResponseHeaders(IHasHeaders hasHeaders, IEnumerable<KeyValuePair<string, string>> responseHeaders)
- {
- foreach (var item in responseHeaders)
- {
- hasHeaders.Headers[item.Key] = item.Value;
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs
deleted file mode 100644
index 980c2cd3a..000000000
--- a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs
+++ /dev/null
@@ -1,212 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Buffers;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Services;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpServer
-{
- public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult
- {
- private const int BufferSize = 81920;
-
- private readonly Dictionary<string, string> _options = new Dictionary<string, string>();
-
- private List<KeyValuePair<long, long?>> _requestedRanges;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="RangeRequestWriter" /> class.
- /// </summary>
- /// <param name="rangeHeader">The range header.</param>
- /// <param name="contentLength">The content length.</param>
- /// <param name="source">The source.</param>
- /// <param name="contentType">Type of the content.</param>
- /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
- public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest)
- {
- if (string.IsNullOrEmpty(contentType))
- {
- throw new ArgumentNullException(nameof(contentType));
- }
-
- RangeHeader = rangeHeader;
- SourceStream = source;
- IsHeadRequest = isHeadRequest;
-
- ContentType = contentType;
- Headers[HeaderNames.ContentType] = contentType;
- Headers[HeaderNames.AcceptRanges] = "bytes";
- StatusCode = HttpStatusCode.PartialContent;
-
- SetRangeValues(contentLength);
- }
-
- /// <summary>
- /// Gets or sets the source stream.
- /// </summary>
- /// <value>The source stream.</value>
- private Stream SourceStream { get; set; }
- private string RangeHeader { get; set; }
- private bool IsHeadRequest { get; set; }
-
- private long RangeStart { get; set; }
- private long RangeEnd { get; set; }
- private long RangeLength { get; set; }
- private long TotalContentLength { get; set; }
-
- public Action OnComplete { get; set; }
-
- /// <summary>
- /// Additional HTTP Headers
- /// </summary>
- /// <value>The headers.</value>
- public IDictionary<string, string> Headers => _options;
-
- /// <summary>
- /// Gets the requested ranges.
- /// </summary>
- /// <value>The requested ranges.</value>
- protected List<KeyValuePair<long, long?>> RequestedRanges
- {
- get
- {
- if (_requestedRanges == null)
- {
- _requestedRanges = new List<KeyValuePair<long, long?>>();
-
- // Example: bytes=0-,32-63
- var ranges = RangeHeader.Split('=')[1].Split(',');
-
- foreach (var range in ranges)
- {
- var vals = range.Split('-');
-
- long start = 0;
- long? end = null;
-
- if (!string.IsNullOrEmpty(vals[0]))
- {
- start = long.Parse(vals[0], CultureInfo.InvariantCulture);
- }
-
- if (!string.IsNullOrEmpty(vals[1]))
- {
- end = long.Parse(vals[1], CultureInfo.InvariantCulture);
- }
-
- _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
- }
- }
-
- return _requestedRanges;
- }
- }
-
- public string ContentType { get; set; }
-
- public IRequest RequestContext { get; set; }
-
- public object Response { get; set; }
-
- public int Status { get; set; }
-
- public HttpStatusCode StatusCode
- {
- get => (HttpStatusCode)Status;
- set => Status = (int)value;
- }
-
- /// <summary>
- /// Sets the range values.
- /// </summary>
- private void SetRangeValues(long contentLength)
- {
- var requestedRange = RequestedRanges[0];
-
- TotalContentLength = contentLength;
-
- // If the requested range is "0-", we can optimize by just doing a stream copy
- if (!requestedRange.Value.HasValue)
- {
- RangeEnd = TotalContentLength - 1;
- }
- else
- {
- RangeEnd = requestedRange.Value.Value;
- }
-
- RangeStart = requestedRange.Key;
- RangeLength = 1 + RangeEnd - RangeStart;
-
- Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture);
- Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
-
- if (RangeStart > 0 && SourceStream.CanSeek)
- {
- SourceStream.Position = RangeStart;
- }
- }
-
- public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
- {
- try
- {
- // Headers only
- if (IsHeadRequest)
- {
- return;
- }
-
- using (var source = SourceStream)
- {
- // If the requested range is "0-", we can optimize by just doing a stream copy
- if (RangeEnd >= TotalContentLength - 1)
- {
- await source.CopyToAsync(responseStream, BufferSize, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- await CopyToInternalAsync(source, responseStream, RangeLength, cancellationToken).ConfigureAwait(false);
- }
- }
- }
- finally
- {
- OnComplete?.Invoke();
- }
- }
-
- private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
- {
- var array = ArrayPool<byte>.Shared.Rent(BufferSize);
- try
- {
- int bytesRead;
- while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
- {
- var bytesToCopy = Math.Min(bytesRead, copyLength);
-
- await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy), cancellationToken).ConfigureAwait(false);
-
- copyLength -= bytesToCopy;
-
- if (copyLength <= 0)
- {
- break;
- }
- }
- }
- finally
- {
- ArrayPool<byte>.Shared.Return(array);
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs
deleted file mode 100644
index a8cd2ac8f..000000000
--- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-using System;
-using System.Globalization;
-using System.Text;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpServer
-{
- /// <summary>
- /// Class ResponseFilter.
- /// </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(IHttpServer server, ILogger logger)
- {
- _server = server;
- _logger = logger;
- }
-
- /// <summary>
- /// Filters the response.
- /// </summary>
- /// <param name="req">The req.</param>
- /// <param name="res">The res.</param>
- /// <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["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)
- {
- _logger.LogError(exception, "Error processing request for {RawUrl}", req.RawUrl);
-
- if (!string.IsNullOrEmpty(exception.Message))
- {
- var error = exception.Message.Replace(Environment.NewLine, " ", StringComparison.Ordinal);
- error = RemoveControlCharacters(error);
-
- res.Headers.Add("X-Application-Error-Code", error);
- }
- }
-
- if (dto is IHasHeaders hasHeaders)
- {
- if (!hasHeaders.Headers.ContainsKey(HeaderNames.Server))
- {
- hasHeaders.Headers[HeaderNames.Server] = "Microsoft-NetCore/2.0, UPnP/1.0 DLNADOC/1.50";
- }
-
- // Content length has to be explicitly set on on HttpListenerResponse or it won't be happy
- if (hasHeaders.Headers.TryGetValue(HeaderNames.ContentLength, out string contentLength)
- && !string.IsNullOrEmpty(contentLength))
- {
- var length = long.Parse(contentLength, CultureInfo.InvariantCulture);
-
- if (length > 0)
- {
- res.ContentLength = length;
- }
- }
- }
- }
-
- /// <summary>
- /// Removes the control characters.
- /// </summary>
- /// <param name="inString">The in string.</param>
- /// <returns>System.String.</returns>
- public static string RemoveControlCharacters(string inString)
- {
- if (inString == null)
- {
- return null;
- }
- else if (inString.Length == 0)
- {
- return inString;
- }
-
- var newString = new StringBuilder(inString.Length);
-
- foreach (var ch in inString)
- {
- if (!char.IsControl(ch))
- {
- newString.Append(ch);
- }
- }
-
- return newString.ToString();
- }
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index 76c1d9bac..68d981ad1 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -1,17 +1,7 @@
#pragma warning disable CS1591
-using System;
-using System.Linq;
-using Emby.Server.Implementations.SocketSharp;
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace Emby.Server.Implementations.HttpServer.Security
@@ -19,32 +9,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
public class AuthService : IAuthService
{
private readonly IAuthorizationContext _authorizationContext;
- private readonly ISessionManager _sessionManager;
- private readonly IServerConfigurationManager _config;
- private readonly INetworkManager _networkManager;
public AuthService(
- IAuthorizationContext authorizationContext,
- IServerConfigurationManager config,
- ISessionManager sessionManager,
- INetworkManager networkManager)
+ IAuthorizationContext authorizationContext)
{
_authorizationContext = authorizationContext;
- _config = config;
- _sessionManager = sessionManager;
- _networkManager = networkManager;
- }
-
- public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes)
- {
- ValidateUser(request, authAttributes);
- }
-
- public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
- {
- var req = new WebSocketSharpRequest(request, null, request.Path);
- var user = ValidateUser(req, authAttributes);
- return user;
}
public AuthorizationInfo Authenticate(HttpRequest request)
@@ -62,185 +31,5 @@ namespace Emby.Server.Implementations.HttpServer.Security
return auth;
}
-
- private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
- {
- // This code is executed before the service
- var auth = _authorizationContext.GetAuthorizationInfo(request);
-
- if (!IsExemptFromAuthenticationToken(authAttributes, request))
- {
- ValidateSecurityToken(request, auth.Token);
- }
-
- if (authAttributes.AllowLocalOnly && !request.IsLocal)
- {
- throw new SecurityException("Operation not found.");
- }
-
- var user = auth.User;
-
- if (user == null && auth.UserId != Guid.Empty)
- {
- throw new AuthenticationException("User with Id " + auth.UserId + " not found");
- }
-
- if (user != null)
- {
- ValidateUserAccess(user, request, authAttributes);
- }
-
- var info = GetTokenInfo(request);
-
- if (!IsExemptFromRoles(auth, authAttributes, request, info))
- {
- var roles = authAttributes.GetRoles();
-
- ValidateRoles(roles, user);
- }
-
- if (!string.IsNullOrEmpty(auth.DeviceId) &&
- !string.IsNullOrEmpty(auth.Client) &&
- !string.IsNullOrEmpty(auth.Device))
- {
- _sessionManager.LogSessionActivity(
- auth.Client,
- auth.Version,
- auth.DeviceId,
- auth.Device,
- request.RemoteIp,
- user);
- }
-
- return user;
- }
-
- private void ValidateUserAccess(
- User user,
- IRequest request,
- IAuthenticationAttributes authAttributes)
- {
- if (user.HasPermission(PermissionKind.IsDisabled))
- {
- throw new SecurityException("User account has been disabled.");
- }
-
- if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !_networkManager.IsInLocalNetwork(request.RemoteIp))
- {
- throw new SecurityException("User account has been disabled.");
- }
-
- if (!user.HasPermission(PermissionKind.IsAdministrator)
- && !authAttributes.EscapeParentalControl
- && !user.IsParentalScheduleAllowed())
- {
- request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl");
-
- throw new SecurityException("This user account is not allowed access at this time.");
- }
- }
-
- private bool IsExemptFromAuthenticationToken(IAuthenticationAttributes authAttribtues, IRequest request)
- {
- if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
- {
- return true;
- }
-
- if (authAttribtues.AllowLocal && request.IsLocal)
- {
- return true;
- }
-
- if (authAttribtues.AllowLocalOnly && request.IsLocal)
- {
- return true;
- }
-
- if (authAttribtues.IgnoreLegacyAuth)
- {
- return true;
- }
-
- return false;
- }
-
- private bool IsExemptFromRoles(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, IRequest request, AuthenticationInfo tokenInfo)
- {
- if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
- {
- return true;
- }
-
- if (authAttribtues.AllowLocal && request.IsLocal)
- {
- return true;
- }
-
- if (authAttribtues.AllowLocalOnly && request.IsLocal)
- {
- return true;
- }
-
- if (string.IsNullOrEmpty(auth.Token))
- {
- return true;
- }
-
- if (tokenInfo != null && tokenInfo.UserId.Equals(Guid.Empty))
- {
- return true;
- }
-
- return false;
- }
-
- private static void ValidateRoles(string[] roles, User user)
- {
- if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase))
- {
- if (user == null || !user.HasPermission(PermissionKind.IsAdministrator))
- {
- throw new SecurityException("User does not have admin access.");
- }
- }
-
- if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase))
- {
- if (user == null || !user.HasPermission(PermissionKind.EnableContentDeletion))
- {
- throw new SecurityException("User does not have delete access.");
- }
- }
-
- if (roles.Contains("download", StringComparer.OrdinalIgnoreCase))
- {
- if (user == null || !user.HasPermission(PermissionKind.EnableContentDownloading))
- {
- throw new SecurityException("User does not have download access.");
- }
- }
- }
-
- private static AuthenticationInfo GetTokenInfo(IRequest request)
- {
- request.Items.TryGetValue("OriginalAuthenticationInfo", out var info);
- return info as AuthenticationInfo;
- }
-
- private void ValidateSecurityToken(IRequest request, string token)
- {
- if (string.IsNullOrEmpty(token))
- {
- throw new AuthenticationException("Access token is required.");
- }
-
- var info = GetTokenInfo(request);
-
- if (info == null)
- {
- throw new AuthenticationException("Access token is invalid or expired.");
- }
- }
}
}
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
index fb93fae3e..4b407dd9d 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
@@ -7,7 +7,6 @@ using System.Net;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
-using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
@@ -24,14 +23,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
_userManager = userManager;
}
- public AuthorizationInfo GetAuthorizationInfo(object requestContext)
+ public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext)
{
- return GetAuthorizationInfo((IRequest)requestContext);
- }
-
- public AuthorizationInfo GetAuthorizationInfo(IRequest requestContext)
- {
- if (requestContext.Items.TryGetValue("AuthorizationInfo", out var cached))
+ if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
{
return (AuthorizationInfo)cached;
}
@@ -52,18 +46,18 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
- private AuthorizationInfo GetAuthorization(IRequest httpReq)
+ private AuthorizationInfo GetAuthorization(HttpContext httpReq)
{
var auth = GetAuthorizationDictionary(httpReq);
var (authInfo, originalAuthInfo) =
- GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString);
+ GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
if (originalAuthInfo != null)
{
- httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
+ httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
}
- httpReq.Items["AuthorizationInfo"] = authInfo;
+ httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
return authInfo;
}
@@ -203,13 +197,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
- private Dictionary<string, string> GetAuthorizationDictionary(IRequest httpReq)
+ private Dictionary<string, string> GetAuthorizationDictionary(HttpContext httpReq)
{
- var auth = httpReq.Headers["X-Emby-Authorization"];
+ var auth = httpReq.Request.Headers["X-Emby-Authorization"];
if (string.IsNullOrEmpty(auth))
{
- auth = httpReq.Headers[HeaderNames.Authorization];
+ auth = httpReq.Request.Headers[HeaderNames.Authorization];
}
return GetAuthorization(auth);
diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
index 03fcfa53d..8777c59b7 100644
--- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
@@ -2,11 +2,11 @@
using System;
using Jellyfin.Data.Entities;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
namespace Emby.Server.Implementations.HttpServer.Security
{
@@ -23,26 +23,20 @@ namespace Emby.Server.Implementations.HttpServer.Security
_sessionManager = sessionManager;
}
- public SessionInfo GetSession(IRequest requestContext)
+ public SessionInfo GetSession(HttpContext requestContext)
{
var authorization = _authContext.GetAuthorizationInfo(requestContext);
var user = authorization.User;
- return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.RemoteIp, user);
- }
-
- private AuthenticationInfo GetTokenInfo(IRequest request)
- {
- request.Items.TryGetValue("OriginalAuthenticationInfo", out var info);
- return info as AuthenticationInfo;
+ return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.Request.RemoteIp(), user);
}
public SessionInfo GetSession(object requestContext)
{
- return GetSession((IRequest)requestContext);
+ return GetSession((HttpContext)requestContext);
}
- public User GetUser(IRequest requestContext)
+ public User GetUser(HttpContext requestContext)
{
var session = GetSession(requestContext);
@@ -51,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
public User GetUser(object requestContext)
{
- return GetUser((IRequest)requestContext);
+ return GetUser((HttpContext)requestContext);
}
}
}
diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs
deleted file mode 100644
index 00e3ab8fe..000000000
--- a/Emby.Server.Implementations/HttpServer/StreamWriter.cs
+++ /dev/null
@@ -1,120 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Services;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpServer
-{
- /// <summary>
- /// Class StreamWriter.
- /// </summary>
- public class StreamWriter : IAsyncStreamWriter, IHasHeaders
- {
- /// <summary>
- /// The options.
- /// </summary>
- private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
-
- /// <summary>
- /// Initializes a new instance of the <see cref="StreamWriter" /> class.
- /// </summary>
- /// <param name="source">The source.</param>
- /// <param name="contentType">Type of the content.</param>
- public StreamWriter(Stream source, string contentType)
- {
- if (string.IsNullOrEmpty(contentType))
- {
- throw new ArgumentNullException(nameof(contentType));
- }
-
- SourceStream = source;
-
- Headers["Content-Type"] = contentType;
-
- if (source.CanSeek)
- {
- Headers[HeaderNames.ContentLength] = source.Length.ToString(CultureInfo.InvariantCulture);
- }
-
- Headers[HeaderNames.ContentType] = contentType;
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="StreamWriter"/> class.
- /// </summary>
- /// <param name="source">The source.</param>
- /// <param name="contentType">Type of the content.</param>
- /// <param name="contentLength">The content length.</param>
- public StreamWriter(byte[] source, string contentType, int contentLength)
- {
- if (string.IsNullOrEmpty(contentType))
- {
- throw new ArgumentNullException(nameof(contentType));
- }
-
- SourceBytes = source;
-
- Headers[HeaderNames.ContentLength] = contentLength.ToString(CultureInfo.InvariantCulture);
- Headers[HeaderNames.ContentType] = contentType;
- }
-
- /// <summary>
- /// Gets or sets the source stream.
- /// </summary>
- /// <value>The source stream.</value>
- private Stream SourceStream { get; set; }
-
- private byte[] SourceBytes { get; set; }
-
- /// <summary>
- /// Gets the options.
- /// </summary>
- /// <value>The options.</value>
- public IDictionary<string, string> Headers => _options;
-
- /// <summary>
- /// Fires when complete.
- /// </summary>
- public Action OnComplete { get; set; }
-
- /// <summary>
- /// Fires when an error occours.
- /// </summary>
- public Action OnError { get; set; }
-
- /// <inheritdoc />
- public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
- {
- try
- {
- var bytes = SourceBytes;
-
- if (bytes != null)
- {
- await responseStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- using (var src = SourceStream)
- {
- await src.CopyToAsync(responseStream, cancellationToken).ConfigureAwait(false);
- }
- }
- }
- catch
- {
- OnError?.Invoke();
-
- throw;
- }
- finally
- {
- OnComplete?.Invoke();
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index d738047e0..7eae4e764 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -179,7 +179,7 @@ namespace Emby.Server.Implementations.HttpServer
return;
}
- WebSocketMessage<object> stub;
+ WebSocketMessage<object>? stub;
try
{
@@ -209,6 +209,12 @@ namespace Emby.Server.Implementations.HttpServer
return;
}
+ if (stub == null)
+ {
+ _logger.LogError("Error processing web socket message");
+ return;
+ }
+
// Tell the PipeReader how much of the buffer we have consumed
reader.AdvanceTo(buffer.End);
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
new file mode 100644
index 000000000..89c1b7ea0
--- /dev/null
+++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
@@ -0,0 +1,102 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+using Jellyfin.Data.Events;
+using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.HttpServer
+{
+ public class WebSocketManager : IWebSocketManager
+ {
+ private readonly ILogger<WebSocketManager> _logger;
+ private readonly ILoggerFactory _loggerFactory;
+
+ private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
+ private bool _disposed = false;
+
+ public WebSocketManager(
+ ILogger<WebSocketManager> logger,
+ ILoggerFactory loggerFactory)
+ {
+ _logger = logger;
+ _loggerFactory = loggerFactory;
+ }
+
+ public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
+
+ /// <inheritdoc />
+ public 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);
+
+ using 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>
+ /// Adds the rest handlers.
+ /// </summary>
+ /// <param name="listeners">The web socket listeners.</param>
+ public void Init(IEnumerable<IWebSocketListener> listeners)
+ {
+ _webSocketListeners = listeners.ToArray();
+ }
+
+ /// <summary>
+ /// Processes the web socket message received.
+ /// </summary>
+ /// <param name="result">The result.</param>
+ private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
+ {
+ if (_disposed)
+ {
+ return Task.CompletedTask;
+ }
+
+ IEnumerable<Task> GetTasks()
+ {
+ foreach (var x in _webSocketListeners)
+ {
+ yield return x.ProcessMessageAsync(result);
+ }
+ }
+
+ return Task.WhenAll(GetTasks());
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 80e09f0a3..09c52d95b 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -13,6 +13,7 @@ using System.Threading.Tasks;
using System.Xml;
using Emby.Server.Implementations.Library;
using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
@@ -29,7 +30,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
index 285a59a24..dd479b7d1 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
@@ -5,8 +5,8 @@ using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using System.Threading;
+using Jellyfin.Data.Events;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Model.Events;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index 5ed6baeb9..a898a564f 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
@@ -24,7 +25,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
diff --git a/Emby.Server.Implementations/Localization/Core/es_DO.json b/Emby.Server.Implementations/Localization/Core/es_DO.json
index 0ef16542f..26732eb3f 100644
--- a/Emby.Server.Implementations/Localization/Core/es_DO.json
+++ b/Emby.Server.Implementations/Localization/Core/es_DO.json
@@ -17,5 +17,8 @@
"Genres": "Géneros",
"Folders": "Carpetas",
"Favorites": "Favoritos",
- "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}"
+ "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}",
+ "HeaderFavoriteSongs": "Canciones Favoritas",
+ "HeaderFavoriteEpisodes": "Episodios Favoritos",
+ "HeaderFavoriteArtists": "Artistas Favoritos"
}
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index d2e27e379..b0dfc312e 100644
--- a/Emby.Server.Implementations/Localization/Core/id.json
+++ b/Emby.Server.Implementations/Localization/Core/id.json
@@ -22,7 +22,7 @@
"HeaderContinueWatching": "Lanjutkan Menonton",
"HeaderCameraUploads": "Unggahan Kamera",
"HeaderAlbumArtists": "Album Artis",
- "Genres": "Genre",
+ "Genres": "Aliran",
"Folders": "Folder",
"Favorites": "Favorit",
"Collections": "Koleksi",
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index 1b55c2e38..d4341f2e8 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -45,7 +45,7 @@
"NameSeasonNumber": "Sesong {0}",
"NameSeasonUnknown": "Sesong ukjent",
"NewVersionIsAvailable": "En ny versjon av Jellyfin Server er tilgjengelig for nedlasting.",
- "NotificationOptionApplicationUpdateAvailable": "Programvareoppdatering er tilgjengelig",
+ "NotificationOptionApplicationUpdateAvailable": "En programvareoppdatering er tilgjengelig",
"NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering installert",
"NotificationOptionAudioPlayback": "Lydavspilling startet",
"NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppet",
diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json
index 576aaeb1b..3f6f3b23c 100644
--- a/Emby.Server.Implementations/Localization/Core/th.json
+++ b/Emby.Server.Implementations/Localization/Core/th.json
@@ -1,73 +1,117 @@
{
"ProviderValue": "ผู้ให้บริการ: {0}",
- "PluginUpdatedWithName": "{0} ได้รับการ update แล้ว",
- "PluginUninstalledWithName": "ถอนการติดตั้ง {0}",
- "PluginInstalledWithName": "{0} ได้รับการติดตั้ง",
- "Plugin": "Plugin",
- "Playlists": "รายการ",
+ "PluginUpdatedWithName": "อัปเดต {0} แล้ว",
+ "PluginUninstalledWithName": "ถอนการติดตั้ง {0} แล้ว",
+ "PluginInstalledWithName": "ติดตั้ง {0} แล้ว",
+ "Plugin": "ปลั๊กอิน",
+ "Playlists": "เพลย์ลิสต์",
"Photos": "รูปภาพ",
- "NotificationOptionVideoPlaybackStopped": "หยุดการเล่น Video",
- "NotificationOptionVideoPlayback": "เริ่มแสดง Video",
- "NotificationOptionUserLockedOut": "ผู้ใช้ Locked Out",
- "NotificationOptionTaskFailed": "ตารางการทำงานล้มเหลว",
- "NotificationOptionServerRestartRequired": "ควร Restart Server",
- "NotificationOptionPluginUpdateInstalled": "Update Plugin แล้ว",
- "NotificationOptionPluginUninstalled": "ถอด Plugin",
- "NotificationOptionPluginInstalled": "ติดตั้ง Plugin แล้ว",
- "NotificationOptionPluginError": "Plugin ล้มเหลว",
- "NotificationOptionNewLibraryContent": "เพิ่มข้อมูลใหม่แล้ว",
- "NotificationOptionInstallationFailed": "ติดตั้งล้มเหลว",
- "NotificationOptionCameraImageUploaded": "รูปภาพถูก upload",
- "NotificationOptionAudioPlaybackStopped": "หยุดการเล่นเสียง",
+ "NotificationOptionVideoPlaybackStopped": "หยุดเล่นวิดีโอ",
+ "NotificationOptionVideoPlayback": "เริ่มเล่นวิดีโอ",
+ "NotificationOptionUserLockedOut": "ผู้ใช้ถูกล็อก",
+ "NotificationOptionTaskFailed": "งานตามกำหนดการล้มเหลว",
+ "NotificationOptionServerRestartRequired": "จำเป็นต้องรีสตาร์ทเซิร์ฟเวอร์",
+ "NotificationOptionPluginUpdateInstalled": "ติดตั้งการอัปเดตปลั๊กอินแล้ว",
+ "NotificationOptionPluginUninstalled": "ถอนการติดตั้งปลั๊กอินแล้ว",
+ "NotificationOptionPluginInstalled": "ติดตั้งปลั๊กอินแล้ว",
+ "NotificationOptionPluginError": "ปลั๊กอินล้มเหลว",
+ "NotificationOptionNewLibraryContent": "เพิ่มเนื้อหาใหม่แล้ว",
+ "NotificationOptionInstallationFailed": "การติดตั้งล้มเหลว",
+ "NotificationOptionCameraImageUploaded": "อัปโหลดภาพถ่ายแล้ว",
+ "NotificationOptionAudioPlaybackStopped": "หยุดเล่นเสียง",
"NotificationOptionAudioPlayback": "เริ่มเล่นเสียง",
- "NotificationOptionApplicationUpdateInstalled": "Update ระบบแล้ว",
- "NotificationOptionApplicationUpdateAvailable": "ระบบ update สามารถใช้ได้แล้ว",
- "NewVersionIsAvailable": "ตรวจพบ Jellyfin เวอร์ชั่นใหม่",
- "NameSeasonUnknown": "ไม่ทราบปี",
- "NameSeasonNumber": "ปี {0}",
- "NameInstallFailed": "{0} ติดตั้งไม่สำเร็จ",
- "MusicVideos": "MV",
- "Music": "เพลง",
- "Movies": "ภาพยนต์",
- "MixedContent": "รายการแบบผสม",
- "MessageServerConfigurationUpdated": "การตั้งค่า update แล้ว",
- "MessageNamedServerConfigurationUpdatedWithValue": "รายการตั้งค่า {0} ได้รับการ update แล้ว",
- "MessageApplicationUpdatedTo": "Jellyfin Server จะ update ไปที่ {0}",
- "MessageApplicationUpdated": "Jellyfin Server update แล้ว",
+ "NotificationOptionApplicationUpdateInstalled": "ติดตั้งการอัปเดตแอพพลิเคชันแล้ว",
+ "NotificationOptionApplicationUpdateAvailable": "มีการอัปเดตแอพพลิเคชัน",
+ "NewVersionIsAvailable": "เวอร์ชันใหม่ของเซิร์ฟเวอร์ Jellyfin พร้อมให้ดาวน์โหลดแล้ว",
+ "NameSeasonUnknown": "ไม่ทราบซีซัน",
+ "NameSeasonNumber": "ซีซัน {0}",
+ "NameInstallFailed": "การติดตั้ง {0} ล้มเหลว",
+ "MusicVideos": "มิวสิควิดีโอ",
+ "Music": "ดนตรี",
+ "Movies": "ภาพยนตร์",
+ "MixedContent": "เนื้อหาผสม",
+ "MessageServerConfigurationUpdated": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์แล้ว",
+ "MessageNamedServerConfigurationUpdatedWithValue": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์ในส่วน {0} แล้ว",
+ "MessageApplicationUpdatedTo": "เซิร์ฟเวอร์ Jellyfin ได้รับการอัปเดตเป็น {0}",
+ "MessageApplicationUpdated": "อัพเดตเซิร์ฟเวอร์ Jellyfin แล้ว",
"Latest": "ล่าสุด",
- "LabelRunningTimeValue": "เวลาที่เล่น : {0}",
- "LabelIpAddressValue": "IP address: {0}",
- "ItemRemovedWithName": "{0} ถูกลบจากรายการ",
- "ItemAddedWithName": "{0} ถูกเพิ่มในรายการ",
- "Inherit": "การสืบทอด",
- "HomeVideos": "วีดีโอส่วนตัว",
- "HeaderRecordingGroups": "ค่ายบันทึก",
+ "LabelRunningTimeValue": "ผ่านไปแล้ว: {0}",
+ "LabelIpAddressValue": "ที่อยู่ IP: {0}",
+ "ItemRemovedWithName": "{0} ถูกลบออกจากไลบรารี",
+ "ItemAddedWithName": "{0} ถูกเพิ่มลงในไลบรารีแล้ว",
+ "Inherit": "สืบทอด",
+ "HomeVideos": "โฮมวิดีโอ",
+ "HeaderRecordingGroups": "กลุ่มการบันทึก",
"HeaderNextUp": "ถัดไป",
- "HeaderLiveTV": "รายการสด",
- "HeaderFavoriteSongs": "เพลงโปรด",
- "HeaderFavoriteShows": "รายการโชว์โปรด",
- "HeaderFavoriteEpisodes": "ฉากโปรด",
- "HeaderFavoriteArtists": "นักแสดงโปรด",
- "HeaderFavoriteAlbums": "อัมบั้มโปรด",
- "HeaderContinueWatching": "ชมต่อจากเดิม",
- "HeaderCameraUploads": "Upload รูปภาพ",
- "HeaderAlbumArtists": "อัลบั้มนักแสดง",
+ "HeaderLiveTV": "ทีวีสด",
+ "HeaderFavoriteSongs": "เพลงที่ชื่นชอบ",
+ "HeaderFavoriteShows": "รายการที่ชื่นชอบ",
+ "HeaderFavoriteEpisodes": "ตอนที่ชื่นชอบ",
+ "HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ",
+ "HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ",
+ "HeaderContinueWatching": "ดูต่อ",
+ "HeaderCameraUploads": "อัปโหลดรูปถ่าย",
+ "HeaderAlbumArtists": "อัลบั้มศิลปิน",
"Genres": "ประเภท",
"Folders": "โฟลเดอร์",
"Favorites": "รายการโปรด",
- "FailedLoginAttemptWithUserName": "การเชื่อมต่อล้มเหลวจาก {0}",
- "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จ",
- "DeviceOfflineWithName": "{0} ตัดการเชื่อมต่อ",
- "Collections": "ชุด",
- "ChapterNameValue": "บทที่ {0}",
- "Channels": "ชาแนล",
- "CameraImageUploadedFrom": "รูปภาพถูก upload จาก {0}",
+ "FailedLoginAttemptWithUserName": "ความพยายามในการเข้าสู่ระบบล้มเหลวจาก {0}",
+ "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จแล้ว",
+ "DeviceOfflineWithName": "{0} ยกเลิกการเชื่อมต่อแล้ว",
+ "Collections": "คอลเลกชัน",
+ "ChapterNameValue": "บท {0}",
+ "Channels": "ช่อง",
+ "CameraImageUploadedFrom": "ภาพถ่ายใหม่ได้ถูกอัปโหลดมาจาก {0}",
"Books": "หนังสือ",
- "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จ",
- "Artists": "นักแสดง",
- "Application": "แอปพลิเคชั่น",
- "AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
+ "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จแล้ว",
+ "Artists": "ศิลปิน",
+ "Application": "แอพพลิเคชัน",
+ "AppDeviceValues": "แอพ: {0}, อุปกรณ์: {1}",
"Albums": "อัลบั้ม",
"ScheduledTaskStartedWithName": "{0} เริ่มต้น",
- "ScheduledTaskFailedWithName": "{0} ล้มเหลว"
+ "ScheduledTaskFailedWithName": "{0} ล้มเหลว",
+ "Songs": "เพลง",
+ "Shows": "รายการ",
+ "ServerNameNeedsToBeRestarted": "{0} ต้องการการรีสตาร์ท",
+ "TaskDownloadMissingSubtitlesDescription": "ค้นหาคำบรรยายที่หายไปในอินเทอร์เน็ตตามค่ากำหนดในข้อมูลเมตา",
+ "TaskDownloadMissingSubtitles": "ดาวน์โหลดคำบรรยายที่ขาดหายไป",
+ "TaskRefreshChannelsDescription": "รีเฟรชข้อมูลช่องอินเทอร์เน็ต",
+ "TaskRefreshChannels": "รีเฟรชช่อง",
+ "TaskCleanTranscodeDescription": "ลบไฟล์ทรานส์โค้ดที่มีอายุมากกว่าหนึ่งวัน",
+ "TaskCleanTranscode": "ล้างไดเรกทอรีทรานส์โค้ด",
+ "TaskUpdatePluginsDescription": "ดาวน์โหลดและติดตั้งโปรแกรมปรับปรุงให้กับปลั๊กอินที่กำหนดค่าให้อัปเดตโดยอัตโนมัติ",
+ "TaskUpdatePlugins": "อัปเดตปลั๊กอิน",
+ "TaskRefreshPeopleDescription": "อัปเดตข้อมูลเมตานักแสดงและผู้กำกับในไลบรารีสื่อ",
+ "TaskRefreshPeople": "รีเฟรชบุคคล",
+ "TaskCleanLogsDescription": "ลบไฟล์บันทึกที่เก่ากว่า {0} วัน",
+ "TaskCleanLogs": "ล้างไดเรกทอรีบันทึก",
+ "TaskRefreshLibraryDescription": "สแกนไลบรารีสื่อของคุณเพื่อหาไฟล์ใหม่และรีเฟรชข้อมูลเมตา",
+ "TaskRefreshLibrary": "สแกนไลบรารีสื่อ",
+ "TaskRefreshChapterImagesDescription": "สร้างภาพขนาดย่อสำหรับวิดีโอที่มีบท",
+ "TaskRefreshChapterImages": "แตกรูปภาพบท",
+ "TaskCleanCacheDescription": "ลบไฟล์แคชที่ระบบไม่ต้องการ",
+ "TaskCleanCache": "ล้างไดเรกทอรีแคช",
+ "TasksChannelsCategory": "ช่องอินเทอร์เน็ต",
+ "TasksApplicationCategory": "แอพพลิเคชัน",
+ "TasksLibraryCategory": "ไลบรารี",
+ "TasksMaintenanceCategory": "ปิดซ่อมบำรุง",
+ "VersionNumber": "เวอร์ชัน {0}",
+ "ValueSpecialEpisodeName": "พิเศษ - {0}",
+ "ValueHasBeenAddedToLibrary": "เพิ่ม {0} ลงในไลบรารีสื่อของคุณแล้ว",
+ "UserStoppedPlayingItemWithValues": "{0} เล่นเสร็จแล้ว {1} บน {2}",
+ "UserStartedPlayingItemWithValues": "{0} กำลังเล่น {1} บน {2}",
+ "UserPolicyUpdatedWithName": "มีการอัปเดตนโยบายผู้ใช้ของ {0}",
+ "UserPasswordChangedWithName": "มีการเปลี่ยนรหัสผ่านของผู้ใช้ {0}",
+ "UserOnlineFromDevice": "{0} ออนไลน์จาก {1}",
+ "UserOfflineFromDevice": "{0} ได้ยกเลิกการเชื่อมต่อจาก {1}",
+ "UserLockedOutWithName": "ผู้ใช้ {0} ถูกล็อก",
+ "UserDownloadingItemWithValues": "{0} กำลังดาวน์โหลด {1}",
+ "UserDeletedWithName": "ลบผู้ใช้ {0} แล้ว",
+ "UserCreatedWithName": "สร้างผู้ใช้ {0} แล้ว",
+ "User": "ผู้ใช้งาน",
+ "TvShows": "รายการทีวี",
+ "System": "ระบบ",
+ "Sync": "ซิงค์",
+ "SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้",
+ "StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่"
}
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
new file mode 100644
index 000000000..140a67541
--- /dev/null
+++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
@@ -0,0 +1,285 @@
+using System;
+using System.Collections.Concurrent;
+using System.Globalization;
+using System.Linq;
+using System.Security.Cryptography;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.QuickConnect;
+using MediaBrowser.Controller.Security;
+using MediaBrowser.Model.QuickConnect;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.QuickConnect
+{
+ /// <summary>
+ /// Quick connect implementation.
+ /// </summary>
+ public class QuickConnectManager : IQuickConnect, IDisposable
+ {
+ private readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider();
+ private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new ConcurrentDictionary<string, QuickConnectResult>();
+
+ private readonly IServerConfigurationManager _config;
+ private readonly ILogger<QuickConnectManager> _logger;
+ private readonly IAuthenticationRepository _authenticationRepository;
+ private readonly IAuthorizationContext _authContext;
+ private readonly IServerApplicationHost _appHost;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="QuickConnectManager"/> class.
+ /// Should only be called at server startup when a singleton is created.
+ /// </summary>
+ /// <param name="config">Configuration.</param>
+ /// <param name="logger">Logger.</param>
+ /// <param name="appHost">Application host.</param>
+ /// <param name="authContext">Authentication context.</param>
+ /// <param name="authenticationRepository">Authentication repository.</param>
+ public QuickConnectManager(
+ IServerConfigurationManager config,
+ ILogger<QuickConnectManager> logger,
+ IServerApplicationHost appHost,
+ IAuthorizationContext authContext,
+ IAuthenticationRepository authenticationRepository)
+ {
+ _config = config;
+ _logger = logger;
+ _appHost = appHost;
+ _authContext = authContext;
+ _authenticationRepository = authenticationRepository;
+
+ ReloadConfiguration();
+ }
+
+ /// <inheritdoc/>
+ public int CodeLength { get; set; } = 6;
+
+ /// <inheritdoc/>
+ public string TokenName { get; set; } = "QuickConnect";
+
+ /// <inheritdoc/>
+ public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable;
+
+ /// <inheritdoc/>
+ public int Timeout { get; set; } = 5;
+
+ private DateTime DateActivated { get; set; }
+
+ /// <inheritdoc/>
+ public void AssertActive()
+ {
+ if (State != QuickConnectState.Active)
+ {
+ throw new ArgumentException("Quick connect is not active on this server");
+ }
+ }
+
+ /// <inheritdoc/>
+ public void Activate()
+ {
+ DateActivated = DateTime.UtcNow;
+ SetState(QuickConnectState.Active);
+ }
+
+ /// <inheritdoc/>
+ public void SetState(QuickConnectState newState)
+ {
+ _logger.LogDebug("Changed quick connect state from {State} to {newState}", State, newState);
+
+ ExpireRequests(true);
+
+ State = newState;
+ _config.Configuration.QuickConnectAvailable = newState == QuickConnectState.Available || newState == QuickConnectState.Active;
+ _config.SaveConfiguration();
+
+ _logger.LogDebug("Configuration saved");
+ }
+
+ /// <inheritdoc/>
+ public QuickConnectResult TryConnect()
+ {
+ ExpireRequests();
+
+ if (State != QuickConnectState.Active)
+ {
+ _logger.LogDebug("Refusing quick connect initiation request, current state is {State}", State);
+ throw new AuthenticationException("Quick connect is not active on this server");
+ }
+
+ var code = GenerateCode();
+ var result = new QuickConnectResult()
+ {
+ Secret = GenerateSecureRandom(),
+ DateAdded = DateTime.UtcNow,
+ Code = code
+ };
+
+ _currentRequests[code] = result;
+ return result;
+ }
+
+ /// <inheritdoc/>
+ public QuickConnectResult CheckRequestStatus(string secret)
+ {
+ ExpireRequests();
+ AssertActive();
+
+ string code = _currentRequests.Where(x => x.Value.Secret == secret).Select(x => x.Value.Code).DefaultIfEmpty(string.Empty).First();
+
+ if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
+ {
+ throw new ResourceNotFoundException("Unable to find request with provided secret");
+ }
+
+ return result;
+ }
+
+ /// <inheritdoc/>
+ public string GenerateCode()
+ {
+ Span<byte> raw = stackalloc byte[4];
+
+ int min = (int)Math.Pow(10, CodeLength - 1);
+ int max = (int)Math.Pow(10, CodeLength);
+
+ uint scale = uint.MaxValue;
+ while (scale == uint.MaxValue)
+ {
+ _rng.GetBytes(raw);
+ scale = BitConverter.ToUInt32(raw);
+ }
+
+ int code = (int)(min + ((max - min) * (scale / (double)uint.MaxValue)));
+ return code.ToString(CultureInfo.InvariantCulture);
+ }
+
+ /// <inheritdoc/>
+ public bool AuthorizeRequest(Guid userId, string code)
+ {
+ ExpireRequests();
+ AssertActive();
+
+ if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
+ {
+ throw new ResourceNotFoundException("Unable to find request");
+ }
+
+ if (result.Authenticated)
+ {
+ throw new InvalidOperationException("Request is already authorized");
+ }
+
+ result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+
+ // Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated.
+ var added = result.DateAdded ?? DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(Timeout));
+ result.DateAdded = added.Subtract(TimeSpan.FromMinutes(Timeout - 1));
+
+ _authenticationRepository.Create(new AuthenticationInfo
+ {
+ AppName = TokenName,
+ AccessToken = result.Authentication,
+ DateCreated = DateTime.UtcNow,
+ DeviceId = _appHost.SystemId,
+ DeviceName = _appHost.FriendlyName,
+ AppVersion = _appHost.ApplicationVersionString,
+ UserId = userId
+ });
+
+ _logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId);
+
+ return true;
+ }
+
+ /// <inheritdoc/>
+ public int DeleteAllDevices(Guid user)
+ {
+ var raw = _authenticationRepository.Get(new AuthenticationInfoQuery()
+ {
+ DeviceId = _appHost.SystemId,
+ UserId = user
+ });
+
+ var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenName, StringComparison.Ordinal));
+
+ var removed = 0;
+ foreach (var token in tokens)
+ {
+ _authenticationRepository.Delete(token);
+ _logger.LogDebug("Deleted token {AccessToken}", token.AccessToken);
+ removed++;
+ }
+
+ return removed;
+ }
+
+ /// <summary>
+ /// Dispose.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Dispose.
+ /// </summary>
+ /// <param name="disposing">Dispose unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _rng?.Dispose();
+ }
+ }
+
+ private string GenerateSecureRandom(int length = 32)
+ {
+ Span<byte> bytes = stackalloc byte[length];
+ _rng.GetBytes(bytes);
+
+ return Hex.Encode(bytes);
+ }
+
+ /// <inheritdoc/>
+ public void ExpireRequests(bool expireAll = false)
+ {
+ // Check if quick connect should be deactivated
+ if (State == QuickConnectState.Active && DateTime.UtcNow > DateActivated.AddMinutes(Timeout) && !expireAll)
+ {
+ _logger.LogDebug("Quick connect time expired, deactivating");
+ SetState(QuickConnectState.Available);
+ expireAll = true;
+ }
+
+ // Expire stale connection requests
+ var code = string.Empty;
+ var values = _currentRequests.Values.ToList();
+
+ for (int i = 0; i < values.Count; i++)
+ {
+ var added = values[i].DateAdded ?? DateTime.UnixEpoch;
+ if (DateTime.UtcNow > added.AddMinutes(Timeout) || expireAll)
+ {
+ code = values[i].Code;
+ _logger.LogDebug("Removing expired request {code}", code);
+
+ if (!_currentRequests.TryRemove(code, out _))
+ {
+ _logger.LogWarning("Request {code} already expired", code);
+ }
+ }
+ }
+ }
+
+ private void ReloadConfiguration()
+ {
+ State = _config.Configuration.QuickConnectAvailable ? QuickConnectState.Available : QuickConnectState.Unavailable;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index 1ef083d04..bc01f9543 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -6,10 +6,10 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
-using MediaBrowser.Model.Events;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
index 6d2b4ffc8..6f81bf49b 100644
--- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
@@ -5,8 +5,8 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Model.Events;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
diff --git a/Emby.Server.Implementations/Services/HttpResult.cs b/Emby.Server.Implementations/Services/HttpResult.cs
deleted file mode 100644
index 8ba86f756..000000000
--- a/Emby.Server.Implementations/Services/HttpResult.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Server.Implementations.Services
-{
- public class HttpResult
- : IHttpResult, IAsyncStreamWriter
- {
- public HttpResult(object response, string contentType, HttpStatusCode statusCode)
- {
- this.Headers = new Dictionary<string, string>();
-
- this.Response = response;
- this.ContentType = contentType;
- this.StatusCode = statusCode;
- }
-
- public object Response { get; set; }
-
- public string ContentType { get; set; }
-
- public IDictionary<string, string> Headers { get; private set; }
-
- public int Status { get; set; }
-
- public HttpStatusCode StatusCode
- {
- get => (HttpStatusCode)Status;
- set => Status = (int)value;
- }
-
- public IRequest RequestContext { get; set; }
-
- public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
- {
- var response = RequestContext?.Response;
-
- if (this.Response is byte[] bytesResponse)
- {
- var contentLength = bytesResponse.Length;
-
- if (response != null)
- {
- response.ContentLength = contentLength;
- }
-
- if (contentLength > 0)
- {
- await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false);
- }
-
- return;
- }
-
- await ResponseHelper.WriteObject(this.RequestContext, this.Response, response).ConfigureAwait(false);
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/RequestHelper.cs b/Emby.Server.Implementations/Services/RequestHelper.cs
deleted file mode 100644
index 1f9c7fc22..000000000
--- a/Emby.Server.Implementations/Services/RequestHelper.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.IO;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-
-namespace Emby.Server.Implementations.Services
-{
- public class RequestHelper
- {
- public static Func<Type, Stream, Task<object>> GetRequestReader(HttpListenerHost host, string contentType)
- {
- switch (GetContentTypeWithoutEncoding(contentType))
- {
- case "application/xml":
- case "text/xml":
- case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
- return host.DeserializeXml;
-
- case "application/json":
- case "text/json":
- return host.DeserializeJson;
- }
-
- return null;
- }
-
- public static Action<object, Stream> GetResponseWriter(HttpListenerHost host, string contentType)
- {
- switch (GetContentTypeWithoutEncoding(contentType))
- {
- case "application/xml":
- case "text/xml":
- case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
- return host.SerializeToXml;
-
- case "application/json":
- case "text/json":
- return host.SerializeToJson;
- }
-
- return null;
- }
-
- private static string GetContentTypeWithoutEncoding(string contentType)
- {
- return contentType?.Split(';')[0].ToLowerInvariant().Trim();
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/ResponseHelper.cs b/Emby.Server.Implementations/Services/ResponseHelper.cs
deleted file mode 100644
index a329b531d..000000000
--- a/Emby.Server.Implementations/Services/ResponseHelper.cs
+++ /dev/null
@@ -1,141 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Globalization;
-using System.IO;
-using System.Net;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-
-namespace Emby.Server.Implementations.Services
-{
- public static class ResponseHelper
- {
- public static Task WriteToResponse(HttpResponse response, IRequest request, object result, CancellationToken cancellationToken)
- {
- if (result == null)
- {
- if (response.StatusCode == (int)HttpStatusCode.OK)
- {
- response.StatusCode = (int)HttpStatusCode.NoContent;
- }
-
- response.ContentLength = 0;
- return Task.CompletedTask;
- }
-
- var httpResult = result as IHttpResult;
- if (httpResult != null)
- {
- httpResult.RequestContext = request;
- request.ResponseContentType = httpResult.ContentType ?? request.ResponseContentType;
- }
-
- var defaultContentType = request.ResponseContentType;
-
- if (httpResult != null)
- {
- if (httpResult.RequestContext == null)
- {
- httpResult.RequestContext = request;
- }
-
- response.StatusCode = httpResult.Status;
- }
-
- if (result is IHasHeaders responseOptions)
- {
- foreach (var responseHeaders in responseOptions.Headers)
- {
- if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))
- {
- response.ContentLength = long.Parse(responseHeaders.Value, CultureInfo.InvariantCulture);
- continue;
- }
-
- response.Headers.Add(responseHeaders.Key, responseHeaders.Value);
- }
- }
-
- // ContentType='text/html' is the default for a HttpResponse
- // Do not override if another has been set
- if (response.ContentType == null || response.ContentType == "text/html")
- {
- response.ContentType = defaultContentType;
- }
-
- if (response.ContentType == "application/json")
- {
- response.ContentType += "; charset=utf-8";
- }
-
- switch (result)
- {
- case IAsyncStreamWriter asyncStreamWriter:
- return asyncStreamWriter.WriteToAsync(response.Body, cancellationToken);
- case IStreamWriter streamWriter:
- streamWriter.WriteTo(response.Body);
- return Task.CompletedTask;
- case FileWriter fileWriter:
- return fileWriter.WriteToAsync(response, cancellationToken);
- case Stream stream:
- return CopyStream(stream, response.Body);
- case byte[] bytes:
- response.ContentType = "application/octet-stream";
- response.ContentLength = bytes.Length;
-
- if (bytes.Length > 0)
- {
- return response.Body.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
- }
-
- return Task.CompletedTask;
- case string responseText:
- var responseTextAsBytes = Encoding.UTF8.GetBytes(responseText);
- response.ContentLength = responseTextAsBytes.Length;
-
- if (responseTextAsBytes.Length > 0)
- {
- return response.Body.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken);
- }
-
- return Task.CompletedTask;
- }
-
- return WriteObject(request, result, response);
- }
-
- private static async Task CopyStream(Stream src, Stream dest)
- {
- using (src)
- {
- await src.CopyToAsync(dest).ConfigureAwait(false);
- }
- }
-
- public static async Task WriteObject(IRequest request, object result, HttpResponse response)
- {
- var contentType = request.ResponseContentType;
- var serializer = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
-
- using (var ms = new MemoryStream())
- {
- serializer(result, ms);
-
- ms.Position = 0;
-
- var contentLength = ms.Length;
- response.ContentLength = contentLength;
-
- if (contentLength > 0)
- {
- await ms.CopyToAsync(response.Body).ConfigureAwait(false);
- }
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs
deleted file mode 100644
index 47e7261e8..000000000
--- a/Emby.Server.Implementations/Services/ServiceController.cs
+++ /dev/null
@@ -1,202 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Services
-{
- public delegate object ActionInvokerFn(object intance, object request);
-
- public delegate void VoidActionInvokerFn(object intance, object request);
-
- public class ServiceController
- {
- private readonly ILogger<ServiceController> _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ServiceController"/> class.
- /// </summary>
- /// <param name="logger">The <see cref="ServiceController"/> logger.</param>
- public ServiceController(ILogger<ServiceController> logger)
- {
- _logger = logger;
- }
-
- public void Init(HttpListenerHost appHost, IEnumerable<Type> serviceTypes)
- {
- foreach (var serviceType in serviceTypes)
- {
- RegisterService(appHost, serviceType);
- }
- }
-
- public void RegisterService(HttpListenerHost appHost, Type serviceType)
- {
- // Make sure the provided type implements IService
- if (!typeof(IService).IsAssignableFrom(serviceType))
- {
- _logger.LogWarning("Tried to register a service that does not implement IService: {ServiceType}", serviceType);
- return;
- }
-
- var processedReqs = new HashSet<Type>();
-
- var actions = ServiceExecGeneral.Reset(serviceType);
-
- foreach (var mi in serviceType.GetActions())
- {
- var requestType = mi.GetParameters()[0].ParameterType;
- if (processedReqs.Contains(requestType))
- {
- continue;
- }
-
- processedReqs.Add(requestType);
-
- ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions);
-
- // var returnMarker = GetTypeWithGenericTypeDefinitionOf(requestType, typeof(IReturn<>));
- // var responseType = returnMarker != null ?
- // GetGenericArguments(returnMarker)[0]
- // : mi.ReturnType != typeof(object) && mi.ReturnType != typeof(void) ?
- // mi.ReturnType
- // : Type.GetType(requestType.FullName + "Response");
-
- RegisterRestPaths(appHost, requestType, serviceType);
-
- appHost.AddServiceInfo(serviceType, requestType);
- }
- }
-
- public readonly RestPath.RestPathMap RestPathMap = new RestPath.RestPathMap();
-
- public void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType)
- {
- var attrs = appHost.GetRouteAttributes(requestType);
- foreach (var attr in attrs)
- {
- var restPath = new RestPath(appHost.CreateInstance, appHost.GetParseFn, requestType, serviceType, attr.Path, attr.Verbs, attr.IsHidden, attr.Summary, attr.Description);
-
- RegisterRestPath(restPath);
- }
- }
-
- private static readonly char[] InvalidRouteChars = new[] { '?', '&' };
-
- public void RegisterRestPath(RestPath restPath)
- {
- if (restPath.Path[0] != '/')
- {
- throw new ArgumentException(
- string.Format(
- CultureInfo.InvariantCulture,
- "Route '{0}' on '{1}' must start with a '/'",
- restPath.Path,
- restPath.RequestType.GetMethodName()));
- }
-
- if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
- {
- throw new ArgumentException(
- string.Format(
- CultureInfo.InvariantCulture,
- "Route '{0}' on '{1}' contains invalid chars. ",
- restPath.Path,
- restPath.RequestType.GetMethodName()));
- }
-
- if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
- {
- pathsAtFirstMatch.Add(restPath);
- }
- else
- {
- RestPathMap[restPath.FirstMatchHashKey] = new List<RestPath>() { restPath };
- }
- }
-
- public RestPath GetRestPathForRequest(string httpMethod, string pathInfo)
- {
- var matchUsingPathParts = RestPath.GetPathPartsForMatching(pathInfo);
-
- List<RestPath> firstMatches;
-
- var yieldedHashMatches = RestPath.GetFirstMatchHashKeys(matchUsingPathParts);
- foreach (var potentialHashMatch in yieldedHashMatches)
- {
- if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches))
- {
- continue;
- }
-
- var bestScore = -1;
- RestPath bestMatch = null;
- foreach (var restPath in firstMatches)
- {
- var score = restPath.MatchScore(httpMethod, matchUsingPathParts);
- if (score > bestScore)
- {
- bestScore = score;
- bestMatch = restPath;
- }
- }
-
- if (bestScore > 0 && bestMatch != null)
- {
- return bestMatch;
- }
- }
-
- var yieldedWildcardMatches = RestPath.GetFirstMatchWildCardHashKeys(matchUsingPathParts);
- foreach (var potentialHashMatch in yieldedWildcardMatches)
- {
- if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches))
- {
- continue;
- }
-
- var bestScore = -1;
- RestPath bestMatch = null;
- foreach (var restPath in firstMatches)
- {
- var score = restPath.MatchScore(httpMethod, matchUsingPathParts);
- if (score > bestScore)
- {
- bestScore = score;
- bestMatch = restPath;
- }
- }
-
- if (bestScore > 0 && bestMatch != null)
- {
- return bestMatch;
- }
- }
-
- return null;
- }
-
- public Task<object> Execute(HttpListenerHost httpHost, object requestDto, IRequest req)
- {
- var requestType = requestDto.GetType();
- req.OperationName = requestType.Name;
-
- var serviceType = httpHost.GetServiceTypeByRequest(requestType);
-
- var service = httpHost.CreateInstance(serviceType);
-
- if (service is IRequiresRequest serviceRequiresContext)
- {
- serviceRequiresContext.Request = req;
- }
-
- // Executes the service and returns the result
- return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName());
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/ServiceExec.cs b/Emby.Server.Implementations/Services/ServiceExec.cs
deleted file mode 100644
index 7b970627e..000000000
--- a/Emby.Server.Implementations/Services/ServiceExec.cs
+++ /dev/null
@@ -1,230 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Linq.Expressions;
-using System.Reflection;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Server.Implementations.Services
-{
- public static class ServiceExecExtensions
- {
- public static string[] AllVerbs = new[] {
- "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", // RFC 2616
- "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", // RFC 2518
- "VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", "UNCHECKOUT",
- "MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY", // RFC 3253
- "ORDERPATCH", // RFC 3648
- "ACL", // RFC 3744
- "PATCH", // https://datatracker.ietf.org/doc/draft-dusseault-http-patch/
- "SEARCH", // https://datatracker.ietf.org/doc/draft-reschke-webdav-search/
- "BCOPY", "BDELETE", "BMOVE", "BPROPFIND", "BPROPPATCH", "NOTIFY",
- "POLL", "SUBSCRIBE", "UNSUBSCRIBE"
- };
-
- public static List<MethodInfo> GetActions(this Type serviceType)
- {
- var list = new List<MethodInfo>();
-
- foreach (var mi in serviceType.GetRuntimeMethods())
- {
- if (!mi.IsPublic)
- {
- continue;
- }
-
- if (mi.IsStatic)
- {
- continue;
- }
-
- if (mi.GetParameters().Length != 1)
- {
- continue;
- }
-
- var actionName = mi.Name;
- if (!AllVerbs.Contains(actionName, StringComparer.OrdinalIgnoreCase))
- {
- continue;
- }
-
- list.Add(mi);
- }
-
- return list;
- }
- }
-
- internal static class ServiceExecGeneral
- {
- private static Dictionary<string, ServiceMethod> execMap = new Dictionary<string, ServiceMethod>();
-
- public static void CreateServiceRunnersFor(Type requestType, List<ServiceMethod> actions)
- {
- foreach (var actionCtx in actions)
- {
- if (execMap.ContainsKey(actionCtx.Id))
- {
- continue;
- }
-
- execMap[actionCtx.Id] = actionCtx;
- }
- }
-
- public static Task<object> Execute(Type serviceType, IRequest request, object instance, object requestDto, string requestName)
- {
- var actionName = request.Verb ?? "POST";
-
- if (execMap.TryGetValue(ServiceMethod.Key(serviceType, actionName, requestName), out ServiceMethod actionContext))
- {
- if (actionContext.RequestFilters != null)
- {
- foreach (var requestFilter in actionContext.RequestFilters)
- {
- requestFilter.RequestFilter(request, request.Response, requestDto);
- if (request.Response.HasStarted)
- {
- Task.FromResult<object>(null);
- }
- }
- }
-
- var response = actionContext.ServiceAction(instance, requestDto);
-
- if (response is Task taskResponse)
- {
- return GetTaskResult(taskResponse);
- }
-
- return Task.FromResult(response);
- }
-
- var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant();
- throw new NotImplementedException(
- string.Format(
- CultureInfo.InvariantCulture,
- "Could not find method named {1}({0}) or Any({0}) on Service {2}",
- requestDto.GetType().GetMethodName(),
- expectedMethodName,
- serviceType.GetMethodName()));
- }
-
- private static async Task<object> GetTaskResult(Task task)
- {
- try
- {
- if (task is Task<object> taskObject)
- {
- return await taskObject.ConfigureAwait(false);
- }
-
- await task.ConfigureAwait(false);
-
- var type = task.GetType().GetTypeInfo();
- if (!type.IsGenericType)
- {
- return null;
- }
-
- var resultProperty = type.GetDeclaredProperty("Result");
- if (resultProperty == null)
- {
- return null;
- }
-
- var result = resultProperty.GetValue(task);
-
- // hack alert
- if (result.GetType().Name.IndexOf("voidtaskresult", StringComparison.OrdinalIgnoreCase) != -1)
- {
- return null;
- }
-
- return result;
- }
- catch (TypeAccessException)
- {
- return null; // return null for void Task's
- }
- }
-
- public static List<ServiceMethod> Reset(Type serviceType)
- {
- var actions = new List<ServiceMethod>();
-
- foreach (var mi in serviceType.GetActions())
- {
- var actionName = mi.Name;
- var args = mi.GetParameters();
-
- var requestType = args[0].ParameterType;
- var actionCtx = new ServiceMethod
- {
- Id = ServiceMethod.Key(serviceType, actionName, requestType.GetMethodName())
- };
-
- actionCtx.ServiceAction = CreateExecFn(serviceType, requestType, mi);
-
- var reqFilters = new List<IHasRequestFilter>();
-
- foreach (var attr in mi.GetCustomAttributes(true))
- {
- if (attr is IHasRequestFilter hasReqFilter)
- {
- reqFilters.Add(hasReqFilter);
- }
- }
-
- if (reqFilters.Count > 0)
- {
- actionCtx.RequestFilters = reqFilters.OrderBy(i => i.Priority).ToArray();
- }
-
- actions.Add(actionCtx);
- }
-
- return actions;
- }
-
- private static ActionInvokerFn CreateExecFn(Type serviceType, Type requestType, MethodInfo mi)
- {
- var serviceParam = Expression.Parameter(typeof(object), "serviceObj");
- var serviceStrong = Expression.Convert(serviceParam, serviceType);
-
- var requestDtoParam = Expression.Parameter(typeof(object), "requestDto");
- var requestDtoStrong = Expression.Convert(requestDtoParam, requestType);
-
- Expression callExecute = Expression.Call(
- serviceStrong, mi, requestDtoStrong);
-
- if (mi.ReturnType != typeof(void))
- {
- var executeFunc = Expression.Lambda<ActionInvokerFn>(
- callExecute,
- serviceParam,
- requestDtoParam).Compile();
-
- return executeFunc;
- }
- else
- {
- var executeFunc = Expression.Lambda<VoidActionInvokerFn>(
- callExecute,
- serviceParam,
- requestDtoParam).Compile();
-
- return (service, request) =>
- {
- executeFunc(service, request);
- return null;
- };
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs
deleted file mode 100644
index b4166f771..000000000
--- a/Emby.Server.Implementations/Services/ServiceHandler.cs
+++ /dev/null
@@ -1,212 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Net.Mime;
-using System.Reflection;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Services
-{
- public class ServiceHandler
- {
- private RestPath _restPath;
-
- private string _responseContentType;
-
- internal ServiceHandler(RestPath restPath, string responseContentType)
- {
- _restPath = restPath;
- _responseContentType = responseContentType;
- }
-
- protected static Task<object> CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType)
- {
- if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0)
- {
- var deserializer = RequestHelper.GetRequestReader(host, contentType);
- if (deserializer != null)
- {
- return deserializer.Invoke(requestType, httpReq.InputStream);
- }
- }
-
- return Task.FromResult(host.CreateInstance(requestType));
- }
-
- public static string GetSanitizedPathInfo(string pathInfo, out string contentType)
- {
- contentType = null;
- var pos = pathInfo.LastIndexOf('.');
- if (pos != -1)
- {
- var format = pathInfo.AsSpan().Slice(pos + 1);
- contentType = GetFormatContentType(format);
- if (contentType != null)
- {
- pathInfo = pathInfo.Substring(0, pos);
- }
- }
-
- return pathInfo;
- }
-
- private static string GetFormatContentType(ReadOnlySpan<char> format)
- {
- if (format.Equals("json", StringComparison.Ordinal))
- {
- return MediaTypeNames.Application.Json;
- }
- else if (format.Equals("xml", StringComparison.Ordinal))
- {
- return MediaTypeNames.Application.Xml;
- }
-
- return null;
- }
-
- public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, CancellationToken cancellationToken)
- {
- httpReq.Items["__route"] = _restPath;
-
- if (_responseContentType != null)
- {
- httpReq.ResponseContentType = _responseContentType;
- }
-
- var request = await CreateRequest(httpHost, httpReq, _restPath).ConfigureAwait(false);
-
- httpHost.ApplyRequestFilters(httpReq, httpRes, request);
-
- httpRes.HttpContext.SetServiceStackRequest(httpReq);
- var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
-
- // Apply response filters
- foreach (var responseFilter in httpHost.ResponseFilters)
- {
- responseFilter(httpReq, httpRes, response);
- }
-
- await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false);
- }
-
- public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath)
- {
- var requestType = restPath.RequestType;
-
- if (RequireqRequestStream(requestType))
- {
- // Used by IRequiresRequestStream
- var requestParams = GetRequestParams(httpReq.Response.HttpContext.Request);
- var request = ServiceHandler.CreateRequest(httpReq, restPath, requestParams, host.CreateInstance(requestType));
-
- var rawReq = (IRequiresRequestStream)request;
- rawReq.RequestStream = httpReq.InputStream;
- return rawReq;
- }
- else
- {
- var requestParams = GetFlattenedRequestParams(httpReq.Response.HttpContext.Request);
-
- var requestDto = await CreateContentTypeRequest(host, httpReq, restPath.RequestType, httpReq.ContentType).ConfigureAwait(false);
-
- return CreateRequest(httpReq, restPath, requestParams, requestDto);
- }
- }
-
- public static bool RequireqRequestStream(Type requestType)
- {
- var requiresRequestStreamTypeInfo = typeof(IRequiresRequestStream).GetTypeInfo();
-
- return requiresRequestStreamTypeInfo.IsAssignableFrom(requestType.GetTypeInfo());
- }
-
- public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary<string, string> requestParams, object requestDto)
- {
- var pathInfo = !restPath.IsWildCardPath
- ? GetSanitizedPathInfo(httpReq.PathInfo, out _)
- : httpReq.PathInfo;
-
- return restPath.CreateRequest(pathInfo, requestParams, requestDto);
- }
-
- /// <summary>
- /// Duplicate Params are given a unique key by appending a #1 suffix
- /// </summary>
- private static Dictionary<string, string> GetRequestParams(HttpRequest request)
- {
- var map = new Dictionary<string, string>();
-
- foreach (var pair in request.Query)
- {
- var values = pair.Value;
- if (values.Count == 1)
- {
- map[pair.Key] = values[0];
- }
- else
- {
- for (var i = 0; i < values.Count; i++)
- {
- map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i];
- }
- }
- }
-
- if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT"))
- && request.HasFormContentType)
- {
- foreach (var pair in request.Form)
- {
- var values = pair.Value;
- if (values.Count == 1)
- {
- map[pair.Key] = values[0];
- }
- else
- {
- for (var i = 0; i < values.Count; i++)
- {
- map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i];
- }
- }
- }
- }
-
- return map;
- }
-
- private static bool IsMethod(string method, string expected)
- => string.Equals(method, expected, StringComparison.OrdinalIgnoreCase);
-
- /// <summary>
- /// Duplicate params have their values joined together in a comma-delimited string.
- /// </summary>
- private static Dictionary<string, string> GetFlattenedRequestParams(HttpRequest request)
- {
- var map = new Dictionary<string, string>();
-
- foreach (var pair in request.Query)
- {
- map[pair.Key] = pair.Value;
- }
-
- if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT"))
- && request.HasFormContentType)
- {
- foreach (var pair in request.Form)
- {
- map[pair.Key] = pair.Value;
- }
- }
-
- return map;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/ServiceMethod.cs b/Emby.Server.Implementations/Services/ServiceMethod.cs
deleted file mode 100644
index 5116cc04f..000000000
--- a/Emby.Server.Implementations/Services/ServiceMethod.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-
-namespace Emby.Server.Implementations.Services
-{
- public class ServiceMethod
- {
- public string Id { get; set; }
-
- public ActionInvokerFn ServiceAction { get; set; }
-
- public MediaBrowser.Model.Services.IHasRequestFilter[] RequestFilters { get; set; }
-
- public static string Key(Type serviceType, string method, string requestDtoName)
- {
- return serviceType.FullName + " " + method.ToUpperInvariant() + " " + requestDtoName;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/ServicePath.cs b/Emby.Server.Implementations/Services/ServicePath.cs
deleted file mode 100644
index 0d4728b43..000000000
--- a/Emby.Server.Implementations/Services/ServicePath.cs
+++ /dev/null
@@ -1,550 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-using System.Text;
-using System.Text.Json.Serialization;
-
-namespace Emby.Server.Implementations.Services
-{
- public class RestPath
- {
- private const string WildCard = "*";
- private const char WildCardChar = '*';
- private const string PathSeperator = "/";
- private const char PathSeperatorChar = '/';
- private const char ComponentSeperator = '.';
- private const string VariablePrefix = "{";
-
- private readonly bool[] componentsWithSeparators;
-
- private readonly string restPath;
- public bool IsWildCardPath { get; private set; }
-
- private readonly string[] literalsToMatch;
-
- private readonly string[] variablesNames;
-
- private readonly bool[] isWildcard;
- private readonly int wildcardCount = 0;
-
- internal static string[] IgnoreAttributesNamed = new[]
- {
- nameof(JsonIgnoreAttribute)
- };
-
- private static Type _excludeType = typeof(Stream);
-
- public int VariableArgsCount { get; set; }
-
- /// <summary>
- /// The number of segments separated by '/' determinable by path.Split('/').Length
- /// e.g. /path/to/here.ext == 3
- /// </summary>
- public int PathComponentsCount { get; set; }
-
- /// <summary>
- /// Gets or sets the total number of segments after subparts have been exploded ('.')
- /// e.g. /path/to/here.ext == 4.
- /// </summary>
- public int TotalComponentsCount { get; set; }
-
- public string[] Verbs { get; private set; }
-
- public Type RequestType { get; private set; }
-
- public Type ServiceType { get; private set; }
-
- public string Path => this.restPath;
-
- public string Summary { get; private set; }
-
- public string Description { get; private set; }
-
- public bool IsHidden { get; private set; }
-
- public static string[] GetPathPartsForMatching(string pathInfo)
- {
- return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- public static List<string> GetFirstMatchHashKeys(string[] pathPartsForMatching)
- {
- var hashPrefix = pathPartsForMatching.Length + PathSeperator;
- return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching);
- }
-
- public static List<string> GetFirstMatchWildCardHashKeys(string[] pathPartsForMatching)
- {
- const string HashPrefix = WildCard + PathSeperator;
- return GetPotentialMatchesWithPrefix(HashPrefix, pathPartsForMatching);
- }
-
- private static List<string> GetPotentialMatchesWithPrefix(string hashPrefix, string[] pathPartsForMatching)
- {
- var list = new List<string>();
-
- foreach (var part in pathPartsForMatching)
- {
- list.Add(hashPrefix + part);
-
- if (part.IndexOf(ComponentSeperator, StringComparison.Ordinal) == -1)
- {
- continue;
- }
-
- var subParts = part.Split(ComponentSeperator);
- foreach (var subPart in subParts)
- {
- list.Add(hashPrefix + subPart);
- }
- }
-
- return list;
- }
-
- public RestPath(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type requestType, Type serviceType, string path, string verbs, bool isHidden = false, string summary = null, string description = null)
- {
- this.RequestType = requestType;
- this.ServiceType = serviceType;
- this.Summary = summary;
- this.IsHidden = isHidden;
- this.Description = description;
- this.restPath = path;
-
- this.Verbs = string.IsNullOrWhiteSpace(verbs) ? ServiceExecExtensions.AllVerbs : verbs.ToUpperInvariant().Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries);
-
- var componentsList = new List<string>();
-
- // We only split on '.' if the restPath has them. Allows for /{action}.{type}
- var hasSeparators = new List<bool>();
- foreach (var component in this.restPath.Split(PathSeperatorChar))
- {
- if (string.IsNullOrEmpty(component))
- {
- continue;
- }
-
- if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1
- && component.IndexOf(ComponentSeperator, StringComparison.Ordinal) != -1)
- {
- hasSeparators.Add(true);
- componentsList.AddRange(component.Split(ComponentSeperator));
- }
- else
- {
- hasSeparators.Add(false);
- componentsList.Add(component);
- }
- }
-
- var components = componentsList.ToArray();
- this.TotalComponentsCount = components.Length;
-
- this.literalsToMatch = new string[this.TotalComponentsCount];
- this.variablesNames = new string[this.TotalComponentsCount];
- this.isWildcard = new bool[this.TotalComponentsCount];
- this.componentsWithSeparators = hasSeparators.ToArray();
- this.PathComponentsCount = this.componentsWithSeparators.Length;
- string firstLiteralMatch = null;
-
- for (var i = 0; i < components.Length; i++)
- {
- var component = components[i];
-
- if (component.StartsWith(VariablePrefix, StringComparison.Ordinal))
- {
- var variableName = component.Substring(1, component.Length - 2);
- if (variableName[variableName.Length - 1] == WildCardChar)
- {
- this.isWildcard[i] = true;
- variableName = variableName.Substring(0, variableName.Length - 1);
- }
-
- this.variablesNames[i] = variableName;
- this.VariableArgsCount++;
- }
- else
- {
- this.literalsToMatch[i] = component.ToLowerInvariant();
-
- if (firstLiteralMatch == null)
- {
- firstLiteralMatch = this.literalsToMatch[i];
- }
- }
- }
-
- for (var i = 0; i < components.Length - 1; i++)
- {
- if (!this.isWildcard[i])
- {
- continue;
- }
-
- if (this.literalsToMatch[i + 1] == null)
- {
- throw new ArgumentException(
- "A wildcard path component must be at the end of the path or followed by a literal path component.");
- }
- }
-
- this.wildcardCount = this.isWildcard.Length;
- this.IsWildCardPath = this.wildcardCount > 0;
-
- this.FirstMatchHashKey = !this.IsWildCardPath
- ? this.PathComponentsCount + PathSeperator + firstLiteralMatch
- : WildCardChar + PathSeperator + firstLiteralMatch;
-
- this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType);
-
- _propertyNamesMap = new HashSet<string>(
- GetSerializableProperties(RequestType).Select(x => x.Name),
- StringComparer.OrdinalIgnoreCase);
- }
-
- internal static IEnumerable<PropertyInfo> GetSerializableProperties(Type type)
- {
- foreach (var prop in GetPublicProperties(type))
- {
- if (prop.GetMethod == null
- || _excludeType == prop.PropertyType)
- {
- continue;
- }
-
- var ignored = false;
- foreach (var attr in prop.GetCustomAttributes(true))
- {
- if (IgnoreAttributesNamed.Contains(attr.GetType().Name))
- {
- ignored = true;
- break;
- }
- }
-
- if (!ignored)
- {
- yield return prop;
- }
- }
- }
-
- private static IEnumerable<PropertyInfo> GetPublicProperties(Type type)
- {
- if (type.IsInterface)
- {
- var propertyInfos = new List<PropertyInfo>();
- var considered = new List<Type>()
- {
- type
- };
- var queue = new Queue<Type>();
- queue.Enqueue(type);
-
- while (queue.Count > 0)
- {
- var subType = queue.Dequeue();
- foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces)
- {
- if (considered.Contains(subInterface))
- {
- continue;
- }
-
- considered.Add(subInterface);
- queue.Enqueue(subInterface);
- }
-
- var newPropertyInfos = GetTypesPublicProperties(subType)
- .Where(x => !propertyInfos.Contains(x));
-
- propertyInfos.InsertRange(0, newPropertyInfos);
- }
-
- return propertyInfos;
- }
-
- return GetTypesPublicProperties(type)
- .Where(x => x.GetIndexParameters().Length == 0);
- }
-
- private static IEnumerable<PropertyInfo> GetTypesPublicProperties(Type subType)
- {
- foreach (var pi in subType.GetRuntimeProperties())
- {
- var mi = pi.GetMethod ?? pi.SetMethod;
- if (mi != null && mi.IsStatic)
- {
- continue;
- }
-
- yield return pi;
- }
- }
-
- /// <summary>
- /// Provide for quick lookups based on hashes that can be determined from a request url.
- /// </summary>
- public string FirstMatchHashKey { get; private set; }
-
- private readonly StringMapTypeDeserializer typeDeserializer;
-
- private readonly HashSet<string> _propertyNamesMap;
-
- public int MatchScore(string httpMethod, string[] withPathInfoParts)
- {
- var isMatch = IsMatch(httpMethod, withPathInfoParts, out var wildcardMatchCount);
- if (!isMatch)
- {
- return -1;
- }
-
- // Routes with least wildcard matches get the highest score
- var score = Math.Max(100 - wildcardMatchCount, 1) * 1000
- // Routes with less variable (and more literal) matches
- + Math.Max(10 - VariableArgsCount, 1) * 100;
-
- // Exact verb match is better than ANY
- if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase))
- {
- score += 10;
- }
- else
- {
- score += 1;
- }
-
- return score;
- }
-
- /// <summary>
- /// For performance withPathInfoParts should already be a lower case string
- /// to minimize redundant matching operations.
- /// </summary>
- public bool IsMatch(string httpMethod, string[] withPathInfoParts, out int wildcardMatchCount)
- {
- wildcardMatchCount = 0;
-
- if (withPathInfoParts.Length != this.PathComponentsCount && !this.IsWildCardPath)
- {
- return false;
- }
-
- if (!Verbs.Contains(httpMethod, StringComparer.OrdinalIgnoreCase))
- {
- return false;
- }
-
- if (!ExplodeComponents(ref withPathInfoParts))
- {
- return false;
- }
-
- if (this.TotalComponentsCount != withPathInfoParts.Length && !this.IsWildCardPath)
- {
- return false;
- }
-
- int pathIx = 0;
- for (var i = 0; i < this.TotalComponentsCount; i++)
- {
- if (this.isWildcard[i])
- {
- if (i < this.TotalComponentsCount - 1)
- {
- // Continue to consume up until a match with the next literal
- while (pathIx < withPathInfoParts.Length
- && !string.Equals(withPathInfoParts[pathIx], this.literalsToMatch[i + 1], StringComparison.InvariantCultureIgnoreCase))
- {
- pathIx++;
- wildcardMatchCount++;
- }
-
- // Ensure there are still enough parts left to match the remainder
- if ((withPathInfoParts.Length - pathIx) < (this.TotalComponentsCount - i - 1))
- {
- return false;
- }
- }
- else
- {
- // A wildcard at the end matches the remainder of path
- wildcardMatchCount += withPathInfoParts.Length - pathIx;
- pathIx = withPathInfoParts.Length;
- }
- }
- else
- {
- var literalToMatch = this.literalsToMatch[i];
- if (literalToMatch == null)
- {
- // Matching an ordinary (non-wildcard) variable consumes a single part
- pathIx++;
- continue;
- }
-
- if (withPathInfoParts.Length <= pathIx
- || !string.Equals(withPathInfoParts[pathIx], literalToMatch, StringComparison.InvariantCultureIgnoreCase))
- {
- return false;
- }
-
- pathIx++;
- }
- }
-
- return pathIx == withPathInfoParts.Length;
- }
-
- private bool ExplodeComponents(ref string[] withPathInfoParts)
- {
- var totalComponents = new List<string>();
- for (var i = 0; i < withPathInfoParts.Length; i++)
- {
- var component = withPathInfoParts[i];
- if (string.IsNullOrEmpty(component))
- {
- continue;
- }
-
- if (this.PathComponentsCount != this.TotalComponentsCount
- && this.componentsWithSeparators[i])
- {
- var subComponents = component.Split(ComponentSeperator);
- if (subComponents.Length < 2)
- {
- return false;
- }
-
- totalComponents.AddRange(subComponents);
- }
- else
- {
- totalComponents.Add(component);
- }
- }
-
- withPathInfoParts = totalComponents.ToArray();
- return true;
- }
-
- public object CreateRequest(string pathInfo, Dictionary<string, string> queryStringAndFormData, object fromInstance)
- {
- var requestComponents = pathInfo.Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
-
- ExplodeComponents(ref requestComponents);
-
- if (requestComponents.Length != this.TotalComponentsCount)
- {
- var isValidWildCardPath = this.IsWildCardPath
- && requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount;
-
- if (!isValidWildCardPath)
- {
- throw new ArgumentException(
- string.Format(
- CultureInfo.InvariantCulture,
- "Path Mismatch: Request Path '{0}' has invalid number of components compared to: '{1}'",
- pathInfo,
- this.restPath));
- }
- }
-
- var requestKeyValuesMap = new Dictionary<string, string>();
- var pathIx = 0;
- for (var i = 0; i < this.TotalComponentsCount; i++)
- {
- var variableName = this.variablesNames[i];
- if (variableName == null)
- {
- pathIx++;
- continue;
- }
-
- if (!this._propertyNamesMap.Contains(variableName))
- {
- if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase))
- {
- pathIx++;
- continue;
- }
-
- throw new ArgumentException("Could not find property "
- + variableName + " on " + RequestType.GetMethodName());
- }
-
- var value = requestComponents.Length > pathIx ? requestComponents[pathIx] : null; // wildcard has arg mismatch
- if (value != null && this.isWildcard[i])
- {
- if (i == this.TotalComponentsCount - 1)
- {
- // Wildcard at end of path definition consumes all the rest
- var sb = new StringBuilder();
- sb.Append(value);
- for (var j = pathIx + 1; j < requestComponents.Length; j++)
- {
- sb.Append(PathSeperatorChar)
- .Append(requestComponents[j]);
- }
-
- value = sb.ToString();
- }
- else
- {
- // Wildcard in middle of path definition consumes up until it
- // hits a match for the next element in the definition (which must be a literal)
- // It may consume 0 or more path parts
- var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1];
- if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
- {
- var sb = new StringBuilder(value);
- pathIx++;
- while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
- {
- sb.Append(PathSeperatorChar)
- .Append(requestComponents[pathIx++]);
- }
-
- value = sb.ToString();
- }
- else
- {
- value = null;
- }
- }
- }
- else
- {
- // Variable consumes single path item
- pathIx++;
- }
-
- requestKeyValuesMap[variableName] = value;
- }
-
- if (queryStringAndFormData != null)
- {
- // Query String and form data can override variable path matches
- // path variables < query string < form data
- foreach (var name in queryStringAndFormData)
- {
- requestKeyValuesMap[name.Key] = name.Value;
- }
- }
-
- return this.typeDeserializer.PopulateFromMap(fromInstance, requestKeyValuesMap);
- }
-
- public class RestPathMap : SortedDictionary<string, List<RestPath>>
- {
- public RestPathMap() : base(StringComparer.OrdinalIgnoreCase)
- {
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
deleted file mode 100644
index 165bb0fc4..000000000
--- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
+++ /dev/null
@@ -1,118 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Reflection;
-using MediaBrowser.Common.Extensions;
-
-namespace Emby.Server.Implementations.Services
-{
- /// <summary>
- /// Serializer cache of delegates required to create a type from a string map (e.g. for REST urls)
- /// </summary>
- public class StringMapTypeDeserializer
- {
- internal class PropertySerializerEntry
- {
- public PropertySerializerEntry(Action<object, object> propertySetFn, Func<string, object> propertyParseStringFn, Type propertyType)
- {
- PropertySetFn = propertySetFn;
- PropertyParseStringFn = propertyParseStringFn;
- PropertyType = propertyType;
- }
-
- public Action<object, object> PropertySetFn { get; private set; }
-
- public Func<string, object> PropertyParseStringFn { get; private set; }
-
- public Type PropertyType { get; private set; }
- }
-
- private readonly Type type;
- private readonly Dictionary<string, PropertySerializerEntry> propertySetterMap
- = new Dictionary<string, PropertySerializerEntry>(StringComparer.OrdinalIgnoreCase);
-
- public Func<string, object> GetParseFn(Type propertyType)
- {
- if (propertyType == typeof(string))
- {
- return s => s;
- }
-
- return _GetParseFn(propertyType);
- }
-
- private readonly Func<Type, object> _CreateInstanceFn;
- private readonly Func<Type, Func<string, object>> _GetParseFn;
-
- public StringMapTypeDeserializer(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type type)
- {
- _CreateInstanceFn = createInstanceFn;
- _GetParseFn = getParseFn;
- this.type = type;
-
- foreach (var propertyInfo in RestPath.GetSerializableProperties(type))
- {
- var propertySetFn = TypeAccessor.GetSetPropertyMethod(propertyInfo);
- var propertyType = propertyInfo.PropertyType;
- var propertyParseStringFn = GetParseFn(propertyType);
- var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType);
-
- propertySetterMap[propertyInfo.Name] = propertySerializer;
- }
- }
-
- public object PopulateFromMap(object instance, IDictionary<string, string> keyValuePairs)
- {
- PropertySerializerEntry propertySerializerEntry = null;
-
- if (instance == null)
- {
- instance = _CreateInstanceFn(type);
- }
-
- foreach (var pair in keyValuePairs)
- {
- string propertyName = pair.Key;
- string propertyTextValue = pair.Value;
-
- if (propertyTextValue == null
- || !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)
- || propertySerializerEntry.PropertySetFn == null)
- {
- continue;
- }
-
- if (propertySerializerEntry.PropertyType == typeof(bool))
- {
- // InputExtensions.cs#530 MVC Checkbox helper emits extra hidden input field, generating 2 values, first is the real value
- propertyTextValue = StringExtensions.LeftPart(propertyTextValue, ',').ToString();
- }
-
- var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue);
- if (value == null)
- {
- continue;
- }
-
- propertySerializerEntry.PropertySetFn(instance, value);
- }
-
- return instance;
- }
- }
-
- internal static class TypeAccessor
- {
- public static Action<object, object> GetSetPropertyMethod(PropertyInfo propertyInfo)
- {
- if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0)
- {
- return null;
- }
-
- var setMethodInfo = propertyInfo.SetMethod;
- return (instance, value) => setMethodInfo.Invoke(instance, new[] { value });
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/UrlExtensions.cs b/Emby.Server.Implementations/Services/UrlExtensions.cs
deleted file mode 100644
index 92e36b60e..000000000
--- a/Emby.Server.Implementations/Services/UrlExtensions.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using MediaBrowser.Common.Extensions;
-
-namespace Emby.Server.Implementations.Services
-{
- /// <summary>
- /// Donated by Ivan Korneliuk from his post:
- /// http://korneliuk.blogspot.com/2012/08/servicestack-reusing-dtos.html
- ///
- /// Modified to only allow using routes matching the supplied HTTP Verb.
- /// </summary>
- public static class UrlExtensions
- {
- public static string GetMethodName(this Type type)
- {
- var typeName = type.FullName != null // can be null, e.g. generic types
- ? StringExtensions.LeftPart(type.FullName, "[[", StringComparison.Ordinal).ToString() // Generic Fullname
- .Replace(type.Namespace + ".", string.Empty, StringComparison.Ordinal) // Trim Namespaces
- .Replace("+", ".", StringComparison.Ordinal) // Convert nested into normal type
- : type.Name;
-
- return type.IsGenericParameter ? "'" + typeName : typeName;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 862a7296c..ca8e0e29b 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
@@ -17,6 +18,8 @@ using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Session;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
@@ -24,7 +27,6 @@ using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
@@ -40,25 +42,16 @@ namespace Emby.Server.Implementations.Session
/// </summary>
public class SessionManager : ISessionManager, IDisposable
{
- /// <summary>
- /// The user data repository.
- /// </summary>
private readonly IUserDataManager _userDataManager;
-
- /// <summary>
- /// The logger.
- /// </summary>
private readonly ILogger<SessionManager> _logger;
-
+ private readonly IEventManager _eventManager;
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IMusicManager _musicManager;
private readonly IDtoService _dtoService;
private readonly IImageProcessor _imageProcessor;
private readonly IMediaSourceManager _mediaSourceManager;
-
private readonly IServerApplicationHost _appHost;
-
private readonly IAuthenticationRepository _authRepo;
private readonly IDeviceManager _deviceManager;
@@ -75,6 +68,7 @@ namespace Emby.Server.Implementations.Session
public SessionManager(
ILogger<SessionManager> logger,
+ IEventManager eventManager,
IUserDataManager userDataManager,
ILibraryManager libraryManager,
IUserManager userManager,
@@ -87,6 +81,7 @@ namespace Emby.Server.Implementations.Session
IMediaSourceManager mediaSourceManager)
{
_logger = logger;
+ _eventManager = eventManager;
_userDataManager = userDataManager;
_libraryManager = libraryManager;
_userManager = userManager;
@@ -209,6 +204,8 @@ namespace Emby.Server.Implementations.Session
}
}
+ _eventManager.Publish(new SessionStartedEventArgs(info));
+
EventHelper.QueueEventIfNotNull(
SessionStarted,
this,
@@ -230,6 +227,8 @@ namespace Emby.Server.Implementations.Session
},
_logger);
+ _eventManager.Publish(new SessionEndedEventArgs(info));
+
info.Dispose();
}
@@ -667,22 +666,26 @@ namespace Emby.Server.Implementations.Session
}
}
+ var eventArgs = new PlaybackProgressEventArgs
+ {
+ Item = libraryItem,
+ Users = users,
+ MediaSourceId = info.MediaSourceId,
+ MediaInfo = info.Item,
+ DeviceName = session.DeviceName,
+ ClientName = session.Client,
+ DeviceId = session.DeviceId,
+ Session = session
+ };
+
+ await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
+
// Nothing to save here
// Fire events to inform plugins
EventHelper.QueueEventIfNotNull(
PlaybackStart,
this,
- new PlaybackProgressEventArgs
- {
- Item = libraryItem,
- Users = users,
- MediaSourceId = info.MediaSourceId,
- MediaInfo = info.Item,
- DeviceName = session.DeviceName,
- ClientName = session.Client,
- DeviceId = session.DeviceId,
- Session = session
- },
+ eventArgs,
_logger);
StartIdleCheckTimer();
@@ -750,23 +753,25 @@ namespace Emby.Server.Implementations.Session
}
}
- PlaybackProgress?.Invoke(
- this,
- new PlaybackProgressEventArgs
- {
- Item = libraryItem,
- Users = users,
- PlaybackPositionTicks = session.PlayState.PositionTicks,
- MediaSourceId = session.PlayState.MediaSourceId,
- MediaInfo = info.Item,
- DeviceName = session.DeviceName,
- ClientName = session.Client,
- DeviceId = session.DeviceId,
- IsPaused = info.IsPaused,
- PlaySessionId = info.PlaySessionId,
- IsAutomated = isAutomated,
- Session = session
- });
+ var eventArgs = new PlaybackProgressEventArgs
+ {
+ Item = libraryItem,
+ Users = users,
+ PlaybackPositionTicks = session.PlayState.PositionTicks,
+ MediaSourceId = session.PlayState.MediaSourceId,
+ MediaInfo = info.Item,
+ DeviceName = session.DeviceName,
+ ClientName = session.Client,
+ DeviceId = session.DeviceId,
+ IsPaused = info.IsPaused,
+ PlaySessionId = info.PlaySessionId,
+ IsAutomated = isAutomated,
+ Session = session
+ };
+
+ await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
+
+ PlaybackProgress?.Invoke(this, eventArgs);
if (!isAutomated)
{
@@ -943,23 +948,23 @@ namespace Emby.Server.Implementations.Session
}
}
- EventHelper.QueueEventIfNotNull(
- PlaybackStopped,
- this,
- new PlaybackStopEventArgs
- {
- Item = libraryItem,
- Users = users,
- PlaybackPositionTicks = info.PositionTicks,
- PlayedToCompletion = playedToCompletion,
- MediaSourceId = info.MediaSourceId,
- MediaInfo = info.Item,
- DeviceName = session.DeviceName,
- ClientName = session.Client,
- DeviceId = session.DeviceId,
- Session = session
- },
- _logger);
+ var eventArgs = new PlaybackStopEventArgs
+ {
+ Item = libraryItem,
+ Users = users,
+ PlaybackPositionTicks = info.PositionTicks,
+ PlayedToCompletion = playedToCompletion,
+ MediaSourceId = info.MediaSourceId,
+ MediaInfo = info.Item,
+ DeviceName = session.DeviceName,
+ ClientName = session.Client,
+ DeviceId = session.DeviceId,
+ Session = session
+ };
+
+ await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
+
+ EventHelper.QueueEventIfNotNull(PlaybackStopped, this, eventArgs, _logger);
}
private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
@@ -1424,6 +1429,24 @@ namespace Emby.Server.Implementations.Session
return AuthenticateNewSessionInternal(request, false);
}
+ public Task<AuthenticationResult> AuthenticateQuickConnect(AuthenticationRequest request, string token)
+ {
+ var result = _authRepo.Get(new AuthenticationInfoQuery()
+ {
+ AccessToken = token,
+ DeviceId = _appHost.SystemId,
+ Limit = 1
+ });
+
+ if (result.TotalRecordCount == 0)
+ {
+ throw new SecurityException("Unknown quick connect token");
+ }
+
+ request.UserId = result.Items[0].UserId;
+ return AuthenticateNewSessionInternal(request, false);
+ }
+
private async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword)
{
CheckDisposed();
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index 8bebd37dc..15c2af220 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -4,9 +4,9 @@ using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Events;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Events;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Session
private readonly ILogger<SessionWebSocketListener> _logger;
private readonly ILoggerFactory _loggerFactory;
- private readonly IHttpServer _httpServer;
+ private readonly IWebSocketManager _webSocketManager;
/// <summary>
/// The KeepAlive cancellation token.
@@ -72,19 +72,19 @@ namespace Emby.Server.Implementations.Session
/// <param name="logger">The logger.</param>
/// <param name="sessionManager">The session manager.</param>
/// <param name="loggerFactory">The logger factory.</param>
- /// <param name="httpServer">The HTTP server.</param>
+ /// <param name="webSocketManager">The HTTP server.</param>
public SessionWebSocketListener(
ILogger<SessionWebSocketListener> logger,
ISessionManager sessionManager,
ILoggerFactory loggerFactory,
- IHttpServer httpServer)
+ IWebSocketManager webSocketManager)
{
_logger = logger;
_sessionManager = sessionManager;
_loggerFactory = loggerFactory;
- _httpServer = httpServer;
+ _webSocketManager = webSocketManager;
- httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
+ webSocketManager.WebSocketConnected += OnServerManagerWebSocketConnected;
}
private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public void Dispose()
{
- _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
+ _webSocketManager.WebSocketConnected -= OnServerManagerWebSocketConnected;
StopKeepAlive();
}
diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
deleted file mode 100644
index ae1a8d0b7..000000000
--- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
+++ /dev/null
@@ -1,248 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Net.Mime;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Primitives;
-using Microsoft.Net.Http.Headers;
-using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest;
-
-namespace Emby.Server.Implementations.SocketSharp
-{
- public class WebSocketSharpRequest : IHttpRequest
- {
- private const string FormUrlEncoded = "application/x-www-form-urlencoded";
- private const string MultiPartFormData = "multipart/form-data";
- private const string Soap11 = "text/xml; charset=utf-8";
-
- private string _remoteIp;
- private Dictionary<string, object> _items;
- private string _responseContentType;
-
- public WebSocketSharpRequest(HttpRequest httpRequest, HttpResponse httpResponse, string operationName)
- {
- this.OperationName = operationName;
- this.Request = httpRequest;
- this.Response = httpResponse;
- }
-
- public string Accept => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Accept]) ? null : Request.Headers[HeaderNames.Accept].ToString();
-
- public string Authorization => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Authorization]) ? null : Request.Headers[HeaderNames.Authorization].ToString();
-
- public HttpRequest Request { get; }
-
- public HttpResponse Response { get; }
-
- public string OperationName { get; set; }
-
- public string RawUrl => Request.GetEncodedPathAndQuery();
-
- public string AbsoluteUri => Request.GetDisplayUrl().TrimEnd('/');
-
- public string RemoteIp
- {
- get
- {
- if (_remoteIp != null)
- {
- return _remoteIp;
- }
-
- IPAddress ip;
-
- // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip
- // (if the server is behind a reverse proxy for example)
- if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XForwardedFor), out ip))
- {
- if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip))
- {
- ip = Request.HttpContext.Connection.RemoteIpAddress;
-
- // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests)
- ip ??= IPAddress.Loopback;
- }
- }
-
- return _remoteIp = NormalizeIp(ip).ToString();
- }
- }
-
- public string[] AcceptTypes => Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
-
- public Dictionary<string, object> Items => _items ?? (_items = new Dictionary<string, object>());
-
- public string ResponseContentType
- {
- get =>
- _responseContentType
- ?? (_responseContentType = GetResponseContentType(Request));
- set => _responseContentType = value;
- }
-
- public string PathInfo => Request.Path.Value;
-
- public string UserAgent => Request.Headers[HeaderNames.UserAgent];
-
- public IHeaderDictionary Headers => Request.Headers;
-
- public IQueryCollection QueryString => Request.Query;
-
- public bool IsLocal =>
- (Request.HttpContext.Connection.LocalIpAddress == null
- && Request.HttpContext.Connection.RemoteIpAddress == null)
- || Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress);
-
- public string HttpMethod => Request.Method;
-
- public string Verb => HttpMethod;
-
- public string ContentType => Request.ContentType;
-
- public Uri UrlReferrer => Request.GetTypedHeaders().Referer;
-
- public Stream InputStream => Request.Body;
-
- public long ContentLength => Request.ContentLength ?? 0;
-
- private string GetHeader(string name) => Request.Headers[name].ToString();
-
- private static IPAddress NormalizeIp(IPAddress ip)
- {
- if (ip.IsIPv4MappedToIPv6)
- {
- return ip.MapToIPv4();
- }
-
- return ip;
- }
-
- public static string GetResponseContentType(HttpRequest httpReq)
- {
- var specifiedContentType = GetQueryStringContentType(httpReq);
- if (!string.IsNullOrEmpty(specifiedContentType))
- {
- return specifiedContentType;
- }
-
- const string ServerDefaultContentType = MediaTypeNames.Application.Json;
-
- var acceptContentTypes = httpReq.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
- string defaultContentType = null;
- if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData))
- {
- defaultContentType = ServerDefaultContentType;
- }
-
- var acceptsAnything = false;
- var hasDefaultContentType = defaultContentType != null;
- if (acceptContentTypes != null)
- {
- foreach (ReadOnlySpan<char> acceptsType in acceptContentTypes)
- {
- ReadOnlySpan<char> contentType = acceptsType;
- var index = contentType.IndexOf(';');
- if (index != -1)
- {
- contentType = contentType.Slice(0, index);
- }
-
- contentType = contentType.Trim();
- acceptsAnything = contentType.Equals("*/*", StringComparison.OrdinalIgnoreCase);
-
- if (acceptsAnything)
- {
- break;
- }
- }
-
- if (acceptsAnything)
- {
- if (hasDefaultContentType)
- {
- return defaultContentType;
- }
- else
- {
- return ServerDefaultContentType;
- }
- }
- }
-
- if (acceptContentTypes == null && httpReq.ContentType == Soap11)
- {
- return Soap11;
- }
-
- // We could also send a '406 Not Acceptable', but this is allowed also
- return ServerDefaultContentType;
- }
-
- public static bool HasAnyOfContentTypes(HttpRequest request, params string[] contentTypes)
- {
- if (contentTypes == null || request.ContentType == null)
- {
- return false;
- }
-
- foreach (var contentType in contentTypes)
- {
- if (IsContentType(request, contentType))
- {
- return true;
- }
- }
-
- return false;
- }
-
- public static bool IsContentType(HttpRequest request, string contentType)
- {
- return request.ContentType.StartsWith(contentType, StringComparison.OrdinalIgnoreCase);
- }
-
- private static string GetQueryStringContentType(HttpRequest httpReq)
- {
- ReadOnlySpan<char> format = httpReq.Query["format"].ToString();
- if (format == ReadOnlySpan<char>.Empty)
- {
- const int FormatMaxLength = 4;
- ReadOnlySpan<char> pi = httpReq.Path.ToString();
- if (pi == null || pi.Length <= FormatMaxLength)
- {
- return null;
- }
-
- if (pi[0] == '/')
- {
- pi = pi.Slice(1);
- }
-
- format = pi.LeftPart('/');
- if (format.Length > FormatMaxLength)
- {
- return null;
- }
- }
-
- format = format.LeftPart('.');
- if (format.Contains("json", StringComparison.OrdinalIgnoreCase))
- {
- return "application/json";
- }
- else if (format.Contains("xml", StringComparison.OrdinalIgnoreCase))
- {
- return "application/xml";
- }
-
- return null;
- }
- }
-}