commit
bebb0afb52
@ -0,0 +1,23 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
internal class ServerItem
|
||||
{
|
||||
public ServerItem(BaseItem item)
|
||||
{
|
||||
Item = item;
|
||||
|
||||
if (item is IItemByName && !(item is Folder))
|
||||
{
|
||||
StubType = Dlna.ContentDirectory.StubType.Folder;
|
||||
}
|
||||
}
|
||||
|
||||
public BaseItem Item { get; set; }
|
||||
|
||||
public StubType? StubType { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1602
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
public enum StubType
|
||||
{
|
||||
Folder = 0,
|
||||
Latest = 2,
|
||||
Playlists = 3,
|
||||
Albums = 4,
|
||||
AlbumArtists = 5,
|
||||
Artists = 6,
|
||||
Songs = 7,
|
||||
Genres = 8,
|
||||
FavoriteSongs = 9,
|
||||
FavoriteArtists = 10,
|
||||
FavoriteAlbums = 11,
|
||||
ContinueWatching = 12,
|
||||
Movies = 13,
|
||||
Collections = 14,
|
||||
Favorites = 15,
|
||||
NextUp = 16,
|
||||
Series = 17,
|
||||
FavoriteSeries = 18,
|
||||
FavoriteEpisodes = 19
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
#nullable enable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public class DlnaConfigurationFactory : IConfigurationFactory
|
||||
{
|
||||
public IEnumerable<ConfigurationStore> GetConfigurations()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new ConfigurationStore
|
||||
{
|
||||
Key = "dlna",
|
||||
ConfigurationType = typeof(DlnaOptions)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public enum TRANSPORTSTATE
|
||||
{
|
||||
STOPPED,
|
||||
PLAYING,
|
||||
TRANSITIONING,
|
||||
PAUSED_PLAYBACK,
|
||||
PAUSED
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1602
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public enum TransportState
|
||||
{
|
||||
Stopped,
|
||||
Playing,
|
||||
Transitioning,
|
||||
PausedPlayback,
|
||||
Paused
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
|
||||
namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IServerEntryPoint" /> which is responsible for starting the library monitor.
|
||||
/// </summary>
|
||||
public sealed class LibraryMonitorStartup : IServerEntryPoint
|
||||
{
|
||||
private readonly ILibraryMonitor _monitor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LibraryMonitorStartup"/> class.
|
||||
/// </summary>
|
||||
/// <param name="monitor">The library monitor.</param>
|
||||
public LibraryMonitorStartup(ILibraryMonitor monitor)
|
||||
{
|
||||
_monitor = monitor;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RunAsync()
|
||||
{
|
||||
_monitor.Start();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue