From 36f3e933a23d802d154c16fd304a82c3fe3f453d Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Wed, 15 Apr 2020 14:28:42 -0500
Subject: Add quick connect
---
.../QuickConnect/IQuickConnect.cs | 91 ++++++++++++++++++++++
MediaBrowser.Controller/Session/ISessionManager.cs | 2 +
2 files changed, 93 insertions(+)
create mode 100644 MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
(limited to 'MediaBrowser.Controller')
diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
new file mode 100644
index 000000000..e4a790ffe
--- /dev/null
+++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.QuickConnect;
+using MediaBrowser.Model.Services;
+
+namespace MediaBrowser.Controller.QuickConnect
+{
+ ///
+ /// Quick connect standard interface.
+ ///
+ public interface IQuickConnect
+ {
+ ///
+ /// Gets or sets the length of user facing codes.
+ ///
+ public int CodeLength { get; set; }
+
+ ///
+ /// Gets or sets the string to prefix internal access tokens with.
+ ///
+ public string TokenNamePrefix { get; set; }
+
+ ///
+ /// Gets the current state of quick connect.
+ ///
+ public QuickConnectState State { get; }
+
+ ///
+ /// Gets or sets the time (in minutes) before a pending request will expire.
+ ///
+ public int RequestExpiry { get; set; }
+
+ ///
+ /// Assert that quick connect is currently active and throws an exception if it is not.
+ ///
+ void AssertActive();
+
+ ///
+ /// Changes the status of quick connect.
+ ///
+ /// New state to change to
+ void SetEnabled(QuickConnectState newState);
+
+ ///
+ /// Initiates a new quick connect request.
+ ///
+ /// Friendly device name to display in the request UI.
+ /// A quick connect result with tokens to proceed or a descriptive error message otherwise.
+ QuickConnectResult TryConnect(string friendlyName);
+
+ ///
+ /// Checks the status of an individual request.
+ ///
+ /// Unique secret identifier of the request.
+ /// Quick connect result.
+ QuickConnectResult CheckRequestStatus(string secret);
+
+ ///
+ /// Returns all current quick connect requests as DTOs. Does not include sensitive information.
+ ///
+ /// List of all quick connect results.
+ List GetCurrentRequests();
+
+ ///
+ /// Returns all current quick connect requests (including sensitive information).
+ ///
+ /// List of all quick connect results.
+ List GetCurrentRequestsInternal();
+
+ ///
+ /// Authorizes a quick connect request to connect as the calling user.
+ ///
+ /// HTTP request object.
+ /// Public request lookup value.
+ /// A boolean indicating if the authorization completed successfully.
+ bool AuthorizeRequest(IRequest request, string lookup);
+
+ ///
+ /// Deletes all quick connect access tokens for the provided user.
+ ///
+ /// Guid of the user to delete tokens for.
+ /// A count of the deleted tokens.
+ int DeleteAllDevices(Guid user);
+
+ ///
+ /// Generates a short code to display to the user to uniquely identify this request.
+ ///
+ /// A short, unique alphanumeric string.
+ string GenerateCode();
+ }
+}
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 771027103..74ffd5a18 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -246,6 +246,8 @@ namespace MediaBrowser.Controller.Session
/// Task{SessionInfo}.
Task AuthenticateNewSession(AuthenticationRequest request);
+ public Task AuthenticateQuickConnect(AuthenticationRequest request, string token);
+
///
/// Creates the new session.
///
--
cgit v1.2.3
From 387a07c6dd4792ea1e77d333e178f9b4e9c56678 Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Sun, 19 Apr 2020 01:33:09 -0500
Subject: Add persistent setting configuration and temporary activation
---
.../QuickConnect/ConfigurationExtension.cs | 30 +++++++++
.../QuickConnect/QuickConnectConfiguration.cs | 13 ++++
.../QuickConnect/QuickConnectManager.cs | 72 +++++++++++++++++++---
.../QuickConnect/QuickConnectService.cs | 40 ++++++++++--
.../QuickConnect/IQuickConnect.cs | 8 ++-
5 files changed, 149 insertions(+), 14 deletions(-)
create mode 100644 Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs
create mode 100644 Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs
(limited to 'MediaBrowser.Controller')
diff --git a/Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs b/Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs
new file mode 100644
index 000000000..0e35ba80a
--- /dev/null
+++ b/Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs
@@ -0,0 +1,30 @@
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+using MediaBrowser.Common.Configuration;
+
+namespace Emby.Server.Implementations.QuickConnect
+{
+ public static class ConfigurationExtension
+ {
+ public static QuickConnectConfiguration GetQuickConnectConfiguration(this IConfigurationManager manager)
+ {
+ return manager.GetConfiguration("quickconnect");
+ }
+ }
+
+ public class QuickConnectConfigurationFactory : IConfigurationFactory
+ {
+ public IEnumerable GetConfigurations()
+ {
+ return new ConfigurationStore[]
+ {
+ new ConfigurationStore
+ {
+ Key = "quickconnect",
+ ConfigurationType = typeof(QuickConnectConfiguration)
+ }
+ };
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs
new file mode 100644
index 000000000..befc46379
--- /dev/null
+++ b/Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs
@@ -0,0 +1,13 @@
+using MediaBrowser.Model.QuickConnect;
+
+namespace Emby.Server.Implementations.QuickConnect
+{
+ public class QuickConnectConfiguration
+ {
+ public QuickConnectConfiguration()
+ {
+ }
+
+ public QuickConnectState State { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
index 30418097c..671ddc2b9 100644
--- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
+++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
@@ -3,7 +3,9 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
+using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.QuickConnect;
@@ -12,6 +14,7 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.QuickConnect;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
+using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.QuickConnect
@@ -24,6 +27,7 @@ namespace Emby.Server.Implementations.QuickConnect
private readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider();
private Dictionary _currentRequests = new Dictionary();
+ private IServerConfigurationManager _config;
private ILogger _logger;
private IUserManager _userManager;
private ILocalizationManager _localizationManager;
@@ -31,11 +35,13 @@ namespace Emby.Server.Implementations.QuickConnect
private IAuthenticationRepository _authenticationRepository;
private IAuthorizationContext _authContext;
private IServerApplicationHost _appHost;
+ private ITaskManager _taskManager;
///
/// Initializes a new instance of the class.
/// Should only be called at server startup when a singleton is created.
///
+ /// Configuration.
/// Logger.
/// User manager.
/// Localization.
@@ -43,15 +49,19 @@ namespace Emby.Server.Implementations.QuickConnect
/// Application host.
/// Authentication context.
/// Authentication repository.
+ /// Task scheduler.
public QuickConnectManager(
+ IServerConfigurationManager config,
ILoggerFactory loggerFactory,
IUserManager userManager,
ILocalizationManager localization,
IJsonSerializer jsonSerializer,
IServerApplicationHost appHost,
IAuthorizationContext authContext,
- IAuthenticationRepository authenticationRepository)
+ IAuthenticationRepository authenticationRepository,
+ ITaskManager taskManager)
{
+ _config = config;
_logger = loggerFactory.CreateLogger(nameof(QuickConnectManager));
_userManager = userManager;
_localizationManager = localization;
@@ -59,6 +69,16 @@ namespace Emby.Server.Implementations.QuickConnect
_appHost = appHost;
_authContext = authContext;
_authenticationRepository = authenticationRepository;
+ _taskManager = taskManager;
+
+ ReloadConfiguration();
+ }
+
+ private void ReloadConfiguration()
+ {
+ var config = _config.GetQuickConnectConfiguration();
+
+ State = config.State;
}
///
@@ -73,6 +93,10 @@ namespace Emby.Server.Implementations.QuickConnect
///
public int RequestExpiry { get; set; } = 30;
+ private bool TemporaryActivation { get; set; } = false;
+
+ private DateTime DateActivated { get; set; }
+
///
public void AssertActive()
{
@@ -82,17 +106,37 @@ namespace Emby.Server.Implementations.QuickConnect
}
}
+ ///
+ public QuickConnectResult Activate()
+ {
+ // This should not call SetEnabled since that would persist the "temporary" activation to the configuration file
+ State = QuickConnectState.Active;
+ DateActivated = DateTime.Now;
+ TemporaryActivation = true;
+
+ return new QuickConnectResult();
+ }
+
///
public void SetEnabled(QuickConnectState newState)
{
_logger.LogDebug("Changed quick connect state from {0} to {1}", State, newState);
State = newState;
+
+ _config.SaveConfiguration("quickconnect", new QuickConnectConfiguration()
+ {
+ State = State
+ });
+
+ _logger.LogDebug("Configuration saved");
}
///
public QuickConnectResult TryConnect(string friendlyName)
{
+ ExpireRequests(true);
+
if (State != QuickConnectState.Active)
{
_logger.LogDebug("Refusing quick connect initiation request, current state is {0}", State);
@@ -122,13 +166,11 @@ namespace Emby.Server.Implementations.QuickConnect
///
public QuickConnectResult CheckRequestStatus(string secret)
{
- AssertActive();
ExpireRequests();
+ AssertActive();
string lookup = _currentRequests.Where(x => x.Value.Secret == secret).Select(x => x.Value.Lookup).DefaultIfEmpty(string.Empty).First();
- _logger.LogDebug("Transformed private identifier {0} into public lookup {1}", secret, lookup);
-
if (!_currentRequests.ContainsKey(lookup))
{
throw new KeyNotFoundException("Unable to find request with provided identifier");
@@ -146,8 +188,8 @@ namespace Emby.Server.Implementations.QuickConnect
///
public List GetCurrentRequestsInternal()
{
- AssertActive();
ExpireRequests();
+ AssertActive();
return _currentRequests.Values.ToList();
}
@@ -174,12 +216,11 @@ namespace Emby.Server.Implementations.QuickConnect
///
public bool AuthorizeRequest(IRequest request, string lookup)
{
+ ExpireRequests();
AssertActive();
var auth = _authContext.GetAuthorizationInfo(request);
- ExpireRequests();
-
if (!_currentRequests.ContainsKey(lookup))
{
throw new KeyNotFoundException("Unable to find request");
@@ -208,6 +249,8 @@ namespace Emby.Server.Implementations.QuickConnect
UserId = auth.UserId
});
+ _logger.LogInformation("Allowing device {0} to login as user {1} with quick connect code {2}", result.FriendlyName, auth.User.Name, result.Code);
+
return true;
}
@@ -239,8 +282,21 @@ namespace Emby.Server.Implementations.QuickConnect
return string.Join(string.Empty, bytes.Select(x => x.ToString("x2", CultureInfo.InvariantCulture)));
}
- private void ExpireRequests()
+ private void ExpireRequests(bool onlyCheckTime = false)
{
+ // check if quick connect should be deactivated
+ if (TemporaryActivation && DateTime.Now > DateActivated.AddMinutes(10) && State == QuickConnectState.Active)
+ {
+ _logger.LogDebug("Quick connect time expired, deactivating");
+ SetEnabled(QuickConnectState.Available);
+ }
+
+ if (onlyCheckTime)
+ {
+ return;
+ }
+
+ // expire stale connection requests
var delete = new List();
var values = _currentRequests.Values.ToList();
diff --git a/MediaBrowser.Api/QuickConnect/QuickConnectService.cs b/MediaBrowser.Api/QuickConnect/QuickConnectService.cs
index 889a78839..60d6ac414 100644
--- a/MediaBrowser.Api/QuickConnect/QuickConnectService.cs
+++ b/MediaBrowser.Api/QuickConnect/QuickConnectService.cs
@@ -98,6 +98,11 @@ namespace MediaBrowser.Api.QuickConnect
public object Get(QuickConnectList request)
{
+ if(_quickConnect.State != QuickConnectState.Active)
+ {
+ return Array.Empty();
+ }
+
return _quickConnect.GetCurrentRequests();
}
@@ -124,15 +129,40 @@ namespace MediaBrowser.Api.QuickConnect
public object Post(Activate request)
{
- if (_quickConnect.State == QuickConnectState.Available)
+ string name = _authContext.GetAuthorizationInfo(Request).User.Name;
+
+ if(_quickConnect.State == QuickConnectState.Unavailable)
+ {
+ return new QuickConnectResult()
+ {
+ Error = "Quick connect is not enabled on this server"
+ };
+ }
+
+ else if(_quickConnect.State == QuickConnectState.Available)
{
- _quickConnect.SetEnabled(QuickConnectState.Active);
+ var result = _quickConnect.Activate();
- string name = _authContext.GetAuthorizationInfo(Request).User.Name;
- Logger.LogInformation("{name} enabled quick connect", name);
+ if (string.IsNullOrEmpty(result.Error))
+ {
+ Logger.LogInformation("{name} temporarily activated quick connect", name);
+ }
+
+ return result;
}
- return _quickConnect.State;
+ else if(_quickConnect.State == QuickConnectState.Active)
+ {
+ return new QuickConnectResult()
+ {
+ Error = ""
+ };
+ }
+
+ return new QuickConnectResult()
+ {
+ Error = "Unknown current state"
+ };
}
public object Post(Available request)
diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
index e4a790ffe..d44765e11 100644
--- a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
+++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
@@ -35,10 +35,16 @@ namespace MediaBrowser.Controller.QuickConnect
///
void AssertActive();
+ ///
+ /// Temporarily activates quick connect for a short amount of time.
+ ///
+ /// A quick connect result object indicating success.
+ QuickConnectResult Activate();
+
///
/// Changes the status of quick connect.
///
- /// New state to change to
+ /// New state to change to.
void SetEnabled(QuickConnectState newState);
///
--
cgit v1.2.3
From 4be476ec5312387f87134915d0fd132b2ad5fa3f Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Thu, 18 Jun 2020 01:29:47 -0500
Subject: Move all settings into the main server configuration
Decreased the timeout from 30 minutes to 5.
Public lookup values have been replaced with the short code.
---
.../QuickConnect/ConfigurationExtension.cs | 20 -------
.../QuickConnect/QuickConnectConfiguration.cs | 15 -----
.../QuickConnectConfigurationFactory.cs | 27 ---------
.../QuickConnect/QuickConnectManager.cs | 66 ++++++++++------------
.../QuickConnect/IQuickConnect.cs | 8 +--
.../Configuration/ServerConfiguration.cs | 6 ++
.../QuickConnect/QuickConnectResult.cs | 5 --
.../QuickConnect/QuickConnectResultDto.cs | 14 +----
8 files changed, 41 insertions(+), 120 deletions(-)
delete mode 100644 Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs
delete mode 100644 Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs
delete mode 100644 Emby.Server.Implementations/QuickConnect/QuickConnectConfigurationFactory.cs
(limited to 'MediaBrowser.Controller')
diff --git a/Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs b/Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs
deleted file mode 100644
index 2a19fc36c..000000000
--- a/Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using MediaBrowser.Common.Configuration;
-
-namespace Emby.Server.Implementations.QuickConnect
-{
- ///
- /// Configuration extension to support persistent quick connect configuration.
- ///
- public static class ConfigurationExtension
- {
- ///
- /// Return the current quick connect configuration.
- ///
- /// Configuration manager.
- /// Current quick connect configuration.
- public static QuickConnectConfiguration GetQuickConnectConfiguration(this IConfigurationManager manager)
- {
- return manager.GetConfiguration("quickconnect");
- }
- }
-}
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs
deleted file mode 100644
index 2302ddbc3..000000000
--- a/Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using MediaBrowser.Model.QuickConnect;
-
-namespace Emby.Server.Implementations.QuickConnect
-{
- ///
- /// Persistent quick connect configuration.
- ///
- public class QuickConnectConfiguration
- {
- ///
- /// Gets or sets persistent quick connect availability state.
- ///
- public QuickConnectState State { get; set; }
- }
-}
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectConfigurationFactory.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectConfigurationFactory.cs
deleted file mode 100644
index d7bc84c5e..000000000
--- a/Emby.Server.Implementations/QuickConnect/QuickConnectConfigurationFactory.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System.Collections.Generic;
-using MediaBrowser.Common.Configuration;
-
-namespace Emby.Server.Implementations.QuickConnect
-{
- ///
- /// Configuration factory for quick connect.
- ///
- public class QuickConnectConfigurationFactory : IConfigurationFactory
- {
- ///
- /// Returns the current quick connect configuration.
- ///
- /// Current quick connect configuration.
- public IEnumerable GetConfigurations()
- {
- return new[]
- {
- new ConfigurationStore
- {
- Key = "quickconnect",
- ConfigurationType = typeof(QuickConnectConfiguration)
- }
- };
- }
- }
-}
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
index 7a584c7cd..8d704f32b 100644
--- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
+++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
@@ -11,7 +11,9 @@ using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.QuickConnect;
using MediaBrowser.Model.Services;
+using MediaBrowser.Common;
using Microsoft.Extensions.Logging;
+using MediaBrowser.Common.Extensions;
namespace Emby.Server.Implementations.QuickConnect
{
@@ -64,9 +66,7 @@ namespace Emby.Server.Implementations.QuickConnect
public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable;
///
- public int RequestExpiry { get; set; } = 30;
-
- private bool TemporaryActivation { get; set; } = false;
+ public int Timeout { get; set; } = 5;
private DateTime DateActivated { get; set; }
@@ -82,10 +82,9 @@ namespace Emby.Server.Implementations.QuickConnect
///
public QuickConnectResult Activate()
{
- // This should not call SetEnabled since that would persist the "temporary" activation to the configuration file
- State = QuickConnectState.Active;
+ SetEnabled(QuickConnectState.Active);
+
DateActivated = DateTime.Now;
- TemporaryActivation = true;
return new QuickConnectResult();
}
@@ -96,12 +95,10 @@ namespace Emby.Server.Implementations.QuickConnect
_logger.LogDebug("Changed quick connect state from {0} to {1}", State, newState);
ExpireRequests(true);
- State = newState;
- _config.SaveConfiguration("quickconnect", new QuickConnectConfiguration()
- {
- State = State
- });
+ State = newState;
+ _config.Configuration.QuickConnectAvailable = newState == QuickConnectState.Available || newState == QuickConnectState.Active;
+ _config.SaveConfiguration();
_logger.LogDebug("Configuration saved");
}
@@ -123,17 +120,16 @@ namespace Emby.Server.Implementations.QuickConnect
_logger.LogDebug("Got new quick connect request from {friendlyName}", friendlyName);
- var lookup = GenerateSecureRandom();
+ var code = GenerateCode();
var result = new QuickConnectResult()
{
- Lookup = lookup,
Secret = GenerateSecureRandom(),
FriendlyName = friendlyName,
DateAdded = DateTime.Now,
- Code = GenerateCode()
+ Code = code
};
- _currentRequests[lookup] = result;
+ _currentRequests[code] = result;
return result;
}
@@ -143,17 +139,16 @@ namespace Emby.Server.Implementations.QuickConnect
ExpireRequests();
AssertActive();
- string lookup = _currentRequests.Where(x => x.Value.Secret == secret).Select(x => x.Value.Lookup).DefaultIfEmpty(string.Empty).First();
+ string code = _currentRequests.Where(x => x.Value.Secret == secret).Select(x => x.Value.Code).DefaultIfEmpty(string.Empty).First();
- if (!_currentRequests.TryGetValue(lookup, out QuickConnectResult result))
+ if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
{
- throw new KeyNotFoundException("Unable to find request with provided identifier");
+ throw new ResourceNotFoundException("Unable to find request with provided secret");
}
return result;
}
- ///
public List GetCurrentRequests()
{
return GetCurrentRequestsInternal().Select(x => (QuickConnectResultDto)x).ToList();
@@ -186,16 +181,16 @@ namespace Emby.Server.Implementations.QuickConnect
}
///
- public bool AuthorizeRequest(IRequest request, string lookup)
+ public bool AuthorizeRequest(IRequest request, string code)
{
ExpireRequests();
AssertActive();
var auth = _authContext.GetAuthorizationInfo(request);
- if (!_currentRequests.TryGetValue(lookup, out QuickConnectResult result))
+ if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
{
- throw new KeyNotFoundException("Unable to find request");
+ throw new ResourceNotFoundException("Unable to find request");
}
if (result.Authenticated)
@@ -205,9 +200,9 @@ namespace Emby.Server.Implementations.QuickConnect
result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
- // Advance the time on the request so it expires sooner as the client will pick up the changes in a few seconds
- var added = result.DateAdded ?? DateTime.Now.Subtract(new TimeSpan(0, RequestExpiry, 0));
- result.DateAdded = added.Subtract(new TimeSpan(0, RequestExpiry - 1, 0));
+ // Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated.
+ var added = result.DateAdded ?? DateTime.Now.Subtract(new TimeSpan(0, Timeout, 0));
+ result.DateAdded = added.Subtract(new TimeSpan(0, Timeout - 1, 0));
_authenticationRepository.Create(new AuthenticationInfo
{
@@ -271,7 +266,7 @@ namespace Emby.Server.Implementations.QuickConnect
var bytes = new byte[length];
_rng.GetBytes(bytes);
- return string.Join(string.Empty, bytes.Select(x => x.ToString("x2", CultureInfo.InvariantCulture)));
+ return Hex.Encode(bytes);
}
///
@@ -281,12 +276,11 @@ namespace Emby.Server.Implementations.QuickConnect
private void ExpireRequests(bool expireAll = false)
{
// Check if quick connect should be deactivated
- if (TemporaryActivation && DateTime.Now > DateActivated.AddMinutes(10) && State == QuickConnectState.Active && !expireAll)
+ if (State == QuickConnectState.Active && DateTime.Now > DateActivated.AddMinutes(Timeout) && !expireAll)
{
_logger.LogDebug("Quick connect time expired, deactivating");
SetEnabled(QuickConnectState.Available);
expireAll = true;
- TemporaryActivation = false;
}
// Expire stale connection requests
@@ -296,28 +290,28 @@ namespace Emby.Server.Implementations.QuickConnect
for (int i = 0; i < values.Count; i++)
{
var added = values[i].DateAdded ?? DateTime.UnixEpoch;
- if (DateTime.Now > added.AddMinutes(RequestExpiry) || expireAll)
+ if (DateTime.Now > added.AddMinutes(Timeout) || expireAll)
{
- delete.Add(values[i].Lookup);
+ delete.Add(values[i].Code);
}
}
- foreach (var lookup in delete)
+ foreach (var code in delete)
{
- _logger.LogDebug("Removing expired request {lookup}", lookup);
+ _logger.LogDebug("Removing expired request {code}", code);
- if (!_currentRequests.TryRemove(lookup, out _))
+ if (!_currentRequests.TryRemove(code, out _))
{
- _logger.LogWarning("Request {lookup} already expired", lookup);
+ _logger.LogWarning("Request {code} already expired", code);
}
}
}
private void ReloadConfiguration()
{
- var config = _config.GetQuickConnectConfiguration();
+ var available = _config.Configuration.QuickConnectAvailable;
- State = config.State;
+ State = available ? QuickConnectState.Available : QuickConnectState.Unavailable;
}
}
}
diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
index d44765e11..d31d0e509 100644
--- a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
+++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
@@ -26,9 +26,9 @@ namespace MediaBrowser.Controller.QuickConnect
public QuickConnectState State { get; }
///
- /// Gets or sets the time (in minutes) before a pending request will expire.
+ /// Gets or sets the time (in minutes) before quick connect will automatically deactivate.
///
- public int RequestExpiry { get; set; }
+ public int Timeout { get; set; }
///
/// Assert that quick connect is currently active and throws an exception if it is not.
@@ -77,9 +77,9 @@ namespace MediaBrowser.Controller.QuickConnect
/// Authorizes a quick connect request to connect as the calling user.
///
/// HTTP request object.
- /// Public request lookup value.
+ /// Identifying code for the request..
/// A boolean indicating if the authorization completed successfully.
- bool AuthorizeRequest(IRequest request, string lookup);
+ bool AuthorizeRequest(IRequest request, string code);
///
/// Deletes all quick connect access tokens for the provided user.
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index afbe02dd3..76b290606 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -76,6 +76,11 @@ namespace MediaBrowser.Model.Configuration
/// true if this instance is port authorized; otherwise, false.
public bool IsPortAuthorized { get; set; }
+ ///
+ /// Gets or sets if quick connect is available for use on this server.
+ ///
+ public bool QuickConnectAvailable { get; set; }
+
public bool AutoRunWebApp { get; set; }
public bool EnableRemoteAccess { get; set; }
@@ -281,6 +286,7 @@ namespace MediaBrowser.Model.Configuration
AutoRunWebApp = true;
EnableRemoteAccess = true;
+ QuickConnectAvailable = false;
EnableUPnP = false;
MinResumePct = 5;
diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs
index 32d7f6aba..a10d60d57 100644
--- a/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs
+++ b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs
@@ -17,11 +17,6 @@ namespace MediaBrowser.Model.QuickConnect
///
public string? Secret { get; set; }
- ///
- /// Gets or sets the public value used to uniquely identify this request. Can only be used to authorize the request.
- ///
- public string? Lookup { get; set; }
-
///
/// Gets or sets the user facing code used so the user can quickly differentiate this request from others.
///
diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs b/MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs
index 19acc7cd8..26084caf1 100644
--- a/MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs
+++ b/MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs
@@ -17,25 +17,15 @@ namespace MediaBrowser.Model.QuickConnect
///
public string? Code { get; private set; }
- ///
- /// Gets the public value used to uniquely identify this request. Can only be used to authorize the request.
- ///
- public string? Lookup { get; private set; }
-
///
/// Gets the device friendly name.
///
public string? FriendlyName { get; private set; }
- ///
- /// Gets the DateTime that this request was created.
- ///
- public DateTime? DateAdded { get; private set; }
-
///
/// Cast an internal quick connect result to a DTO by removing all sensitive properties.
///
- /// QuickConnectResult object to cast
+ /// QuickConnectResult object to cast.
public static implicit operator QuickConnectResultDto(QuickConnectResult result)
{
QuickConnectResultDto resultDto = new QuickConnectResultDto
@@ -43,8 +33,6 @@ namespace MediaBrowser.Model.QuickConnect
Authenticated = result.Authenticated,
Code = result.Code,
FriendlyName = result.FriendlyName,
- DateAdded = result.DateAdded,
- Lookup = result.Lookup
};
return resultDto;
--
cgit v1.2.3
From 329980c727cf03587ff5f4011a3af3ef2fa5e4f1 Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Thu, 18 Jun 2020 01:58:58 -0500
Subject: API cleanup
---
.../QuickConnect/QuickConnectManager.cs | 35 +++--------
.../QuickConnect/QuickConnectService.cs | 67 ++++------------------
.../QuickConnect/IQuickConnect.cs | 23 +++-----
.../QuickConnect/QuickConnectResultDto.cs | 41 -------------
4 files changed, 27 insertions(+), 139 deletions(-)
delete mode 100644 MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs
(limited to 'MediaBrowser.Controller')
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
index 8d704f32b..263556e9d 100644
--- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
+++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
@@ -75,18 +75,15 @@ namespace Emby.Server.Implementations.QuickConnect
{
if (State != QuickConnectState.Active)
{
- throw new InvalidOperationException("Quick connect is not active on this server");
+ throw new ArgumentException("Quick connect is not active on this server");
}
}
///
- public QuickConnectResult Activate()
+ public void Activate()
{
- SetEnabled(QuickConnectState.Active);
-
DateActivated = DateTime.Now;
-
- return new QuickConnectResult();
+ SetEnabled(QuickConnectState.Active);
}
///
@@ -149,19 +146,6 @@ namespace Emby.Server.Implementations.QuickConnect
return result;
}
- public List GetCurrentRequests()
- {
- return GetCurrentRequestsInternal().Select(x => (QuickConnectResultDto)x).ToList();
- }
-
- ///
- public List GetCurrentRequestsInternal()
- {
- ExpireRequests();
- AssertActive();
- return _currentRequests.Values.ToList();
- }
-
///
public string GenerateCode()
{
@@ -215,7 +199,7 @@ namespace Emby.Server.Implementations.QuickConnect
UserId = auth.UserId
});
- _logger.LogInformation("Allowing device {0} to login as user {1} with quick connect code {2}", result.FriendlyName, auth.User.Name, result.Code);
+ _logger.LogInformation("Allowing device {0} to login as user {1} with quick connect code {2}", result.FriendlyName, auth.User.Username, result.Code);
return true;
}
@@ -269,11 +253,8 @@ namespace Emby.Server.Implementations.QuickConnect
return Hex.Encode(bytes);
}
- ///
- /// Expire quick connect requests that are over the time limit. If is true, all requests are unconditionally expired.
- ///
- /// If true, all requests will be expired.
- private void ExpireRequests(bool expireAll = false)
+ ///
+ public void ExpireRequests(bool expireAll = false)
{
// Check if quick connect should be deactivated
if (State == QuickConnectState.Active && DateTime.Now > DateActivated.AddMinutes(Timeout) && !expireAll)
@@ -309,9 +290,7 @@ namespace Emby.Server.Implementations.QuickConnect
private void ReloadConfiguration()
{
- var available = _config.Configuration.QuickConnectAvailable;
-
- State = available ? QuickConnectState.Available : QuickConnectState.Unavailable;
+ State = _config.Configuration.QuickConnectAvailable ? QuickConnectState.Available : QuickConnectState.Unavailable;
}
}
}
diff --git a/MediaBrowser.Api/QuickConnect/QuickConnectService.cs b/MediaBrowser.Api/QuickConnect/QuickConnectService.cs
index 60d6ac414..9047a1e95 100644
--- a/MediaBrowser.Api/QuickConnect/QuickConnectService.cs
+++ b/MediaBrowser.Api/QuickConnect/QuickConnectService.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
@@ -24,18 +23,12 @@ namespace MediaBrowser.Api.QuickConnect
public string Secret { get; set; }
}
- [Route("/QuickConnect/List", "GET", Summary = "Lists all quick connect requests")]
- [Authenticated]
- public class QuickConnectList : IReturn>
- {
- }
-
[Route("/QuickConnect/Authorize", "POST", Summary = "Authorizes a pending quick connect request")]
[Authenticated]
- public class Authorize : IReturn
+ public class Authorize : IReturn
{
- [ApiMember(Name = "Lookup", Description = "Quick connect public lookup", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Lookup { get; set; }
+ [ApiMember(Name = "Code", Description = "Quick connect identifying code", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
+ public string Code { get; set; }
}
[Route("/QuickConnect/Deauthorize", "POST", Summary = "Deletes all quick connect authorization tokens for the current user")]
@@ -62,8 +55,9 @@ namespace MediaBrowser.Api.QuickConnect
[Route("/QuickConnect/Activate", "POST", Summary = "Temporarily activates quick connect for the time period defined in the server configuration")]
[Authenticated]
- public class Activate : IReturn
+ public class Activate : IReturn
{
+
}
public class QuickConnectService : BaseApiService
@@ -96,18 +90,9 @@ namespace MediaBrowser.Api.QuickConnect
return _quickConnect.CheckRequestStatus(request.Secret);
}
- public object Get(QuickConnectList request)
- {
- if(_quickConnect.State != QuickConnectState.Active)
- {
- return Array.Empty();
- }
-
- return _quickConnect.GetCurrentRequests();
- }
-
public object Get(QuickConnectStatus request)
{
+ _quickConnect.ExpireRequests();
return _quickConnect.State;
}
@@ -120,55 +105,27 @@ namespace MediaBrowser.Api.QuickConnect
public object Post(Authorize request)
{
- bool result = _quickConnect.AuthorizeRequest(Request, request.Lookup);
-
- Logger.LogInformation("Result of authorizing quick connect {0}: {1}", request.Lookup[..10], result);
-
- return result;
+ return _quickConnect.AuthorizeRequest(Request, request.Code);
}
public object Post(Activate request)
{
- string name = _authContext.GetAuthorizationInfo(Request).User.Name;
-
if(_quickConnect.State == QuickConnectState.Unavailable)
{
- return new QuickConnectResult()
- {
- Error = "Quick connect is not enabled on this server"
- };
+ return false;
}
- else if(_quickConnect.State == QuickConnectState.Available)
- {
- var result = _quickConnect.Activate();
-
- if (string.IsNullOrEmpty(result.Error))
- {
- Logger.LogInformation("{name} temporarily activated quick connect", name);
- }
+ string name = _authContext.GetAuthorizationInfo(Request).User.Username;
- return result;
- }
+ Logger.LogInformation("{name} temporarily activated quick connect", name);
+ _quickConnect.Activate();
- else if(_quickConnect.State == QuickConnectState.Active)
- {
- return new QuickConnectResult()
- {
- Error = ""
- };
- }
-
- return new QuickConnectResult()
- {
- Error = "Unknown current state"
- };
+ return true;
}
public object Post(Available request)
{
_quickConnect.SetEnabled(request.Status);
-
return _quickConnect.State;
}
}
diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
index d31d0e509..10ec9e6cb 100644
--- a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
+++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
@@ -38,8 +38,7 @@ namespace MediaBrowser.Controller.QuickConnect
///
/// Temporarily activates quick connect for a short amount of time.
///
- /// A quick connect result object indicating success.
- QuickConnectResult Activate();
+ void Activate();
///
/// Changes the status of quick connect.
@@ -61,26 +60,20 @@ namespace MediaBrowser.Controller.QuickConnect
/// Quick connect result.
QuickConnectResult CheckRequestStatus(string secret);
- ///
- /// Returns all current quick connect requests as DTOs. Does not include sensitive information.
- ///
- /// List of all quick connect results.
- List GetCurrentRequests();
-
- ///
- /// Returns all current quick connect requests (including sensitive information).
- ///
- /// List of all quick connect results.
- List GetCurrentRequestsInternal();
-
///
/// Authorizes a quick connect request to connect as the calling user.
///
/// HTTP request object.
- /// Identifying code for the request..
+ /// Identifying code for the request.
/// A boolean indicating if the authorization completed successfully.
bool AuthorizeRequest(IRequest request, string code);
+ ///
+ /// Expire quick connect requests that are over the time limit. If is true, all requests are unconditionally expired.
+ ///
+ /// If true, all requests will be expired.
+ public void ExpireRequests(bool expireAll = false);
+
///
/// Deletes all quick connect access tokens for the provided user.
///
diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs b/MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs
deleted file mode 100644
index 26084caf1..000000000
--- a/MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using System;
-
-namespace MediaBrowser.Model.QuickConnect
-{
- ///
- /// Stores the non-sensitive results of an incoming quick connect request.
- ///
- public class QuickConnectResultDto
- {
- ///
- /// Gets a value indicating whether this request is authorized.
- ///
- public bool Authenticated { get; private set; }
-
- ///
- /// Gets the user facing code used so the user can quickly differentiate this request from others.
- ///
- public string? Code { get; private set; }
-
- ///
- /// Gets the device friendly name.
- ///
- public string? FriendlyName { get; private set; }
-
- ///
- /// Cast an internal quick connect result to a DTO by removing all sensitive properties.
- ///
- /// QuickConnectResult object to cast.
- public static implicit operator QuickConnectResultDto(QuickConnectResult result)
- {
- QuickConnectResultDto resultDto = new QuickConnectResultDto
- {
- Authenticated = result.Authenticated,
- Code = result.Code,
- FriendlyName = result.FriendlyName,
- };
-
- return resultDto;
- }
- }
-}
--
cgit v1.2.3
From 3c91aa0c3d4af3c3d11b4c732ea14c7e641ba662 Mon Sep 17 00:00:00 2001
From: Matt Montgomery <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Sun, 26 Jul 2020 23:13:14 -0500
Subject: Code cleanup
---
.../QuickConnect/QuickConnectManager.cs | 25 +++++++++++-----------
.../QuickConnect/QuickConnectService.cs | 2 +-
.../QuickConnect/IQuickConnect.cs | 6 +++---
3 files changed, 16 insertions(+), 17 deletions(-)
(limited to 'MediaBrowser.Controller')
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
index a69ea2267..23e94afd7 100644
--- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
+++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
@@ -14,6 +14,7 @@ using MediaBrowser.Model.Services;
using MediaBrowser.Common;
using Microsoft.Extensions.Logging;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Authentication;
namespace Emby.Server.Implementations.QuickConnect
{
@@ -83,13 +84,13 @@ namespace Emby.Server.Implementations.QuickConnect
public void Activate()
{
DateActivated = DateTime.Now;
- SetEnabled(QuickConnectState.Active);
+ SetState(QuickConnectState.Active);
}
///
- public void SetEnabled(QuickConnectState newState)
+ public void SetState(QuickConnectState newState)
{
- _logger.LogDebug("Changed quick connect state from {0} to {1}", State, newState);
+ _logger.LogDebug("Changed quick connect state from {State} to {newState}", State, newState);
ExpireRequests(true);
@@ -107,12 +108,8 @@ namespace Emby.Server.Implementations.QuickConnect
if (State != QuickConnectState.Active)
{
- _logger.LogDebug("Refusing quick connect initiation request, current state is {0}", State);
-
- return new QuickConnectResult()
- {
- Error = "Quick connect is not active on this server"
- };
+ _logger.LogDebug("Refusing quick connect initiation request, current state is {State}", State);
+ throw new AuthenticationException("Quick connect is not active on this server");
}
_logger.LogDebug("Got new quick connect request from {friendlyName}", friendlyName);
@@ -200,7 +197,7 @@ namespace Emby.Server.Implementations.QuickConnect
UserId = auth.UserId
});
- _logger.LogInformation("Allowing device {0} to login as user {1} with quick connect code {2}", result.FriendlyName, auth.User.Username, result.Code);
+ _logger.LogInformation("Allowing device {FriendlyName} to login as user {Username} with quick connect code {Code}", result.FriendlyName, auth.User.Username, result.Code);
return true;
}
@@ -216,13 +213,15 @@ namespace Emby.Server.Implementations.QuickConnect
var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenNamePrefix, StringComparison.CurrentCulture));
+ var removed = 0;
foreach (var token in tokens)
{
_authenticationRepository.Delete(token);
- _logger.LogDebug("Deleted token {0}", token.AccessToken);
+ _logger.LogDebug("Deleted token {AccessToken}", token.AccessToken);
+ removed++;
}
- return tokens.Count();
+ return removed;
}
///
@@ -261,7 +260,7 @@ namespace Emby.Server.Implementations.QuickConnect
if (State == QuickConnectState.Active && DateTime.Now > DateActivated.AddMinutes(Timeout) && !expireAll)
{
_logger.LogDebug("Quick connect time expired, deactivating");
- SetEnabled(QuickConnectState.Available);
+ SetState(QuickConnectState.Available);
expireAll = true;
}
diff --git a/MediaBrowser.Api/QuickConnect/QuickConnectService.cs b/MediaBrowser.Api/QuickConnect/QuickConnectService.cs
index 6298f66e5..7093be990 100644
--- a/MediaBrowser.Api/QuickConnect/QuickConnectService.cs
+++ b/MediaBrowser.Api/QuickConnect/QuickConnectService.cs
@@ -125,7 +125,7 @@ namespace MediaBrowser.Api.QuickConnect
public object Post(Available request)
{
- _quickConnect.SetEnabled(request.Status);
+ _quickConnect.SetState(request.Status);
return _quickConnect.State;
}
}
diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
index 10ec9e6cb..5518e0385 100644
--- a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
+++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
@@ -41,16 +41,16 @@ namespace MediaBrowser.Controller.QuickConnect
void Activate();
///
- /// Changes the status of quick connect.
+ /// Changes the state of quick connect.
///
/// New state to change to.
- void SetEnabled(QuickConnectState newState);
+ void SetState(QuickConnectState newState);
///
/// Initiates a new quick connect request.
///
/// Friendly device name to display in the request UI.
- /// A quick connect result with tokens to proceed or a descriptive error message otherwise.
+ /// A quick connect result with tokens to proceed or throws an exception if not active.
QuickConnectResult TryConnect(string friendlyName);
///
--
cgit v1.2.3
From 31d3b1b83aa356221e8af2f316b58584579207fe Mon Sep 17 00:00:00 2001
From: Matt Montgomery <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Mon, 27 Jul 2020 15:40:14 -0500
Subject: Cleanup interfaces
---
MediaBrowser.Controller/QuickConnect/IQuickConnect.cs | 10 +++++-----
MediaBrowser.Controller/Session/ISessionManager.cs | 7 ++++++-
2 files changed, 11 insertions(+), 6 deletions(-)
(limited to 'MediaBrowser.Controller')
diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
index 5518e0385..993637c8a 100644
--- a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
+++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
@@ -13,22 +13,22 @@ namespace MediaBrowser.Controller.QuickConnect
///
/// Gets or sets the length of user facing codes.
///
- public int CodeLength { get; set; }
+ int CodeLength { get; set; }
///
/// Gets or sets the string to prefix internal access tokens with.
///
- public string TokenNamePrefix { get; set; }
+ string TokenNamePrefix { get; set; }
///
/// Gets the current state of quick connect.
///
- public QuickConnectState State { get; }
+ QuickConnectState State { get; }
///
/// Gets or sets the time (in minutes) before quick connect will automatically deactivate.
///
- public int Timeout { get; set; }
+ int Timeout { get; set; }
///
/// Assert that quick connect is currently active and throws an exception if it is not.
@@ -72,7 +72,7 @@ namespace MediaBrowser.Controller.QuickConnect
/// Expire quick connect requests that are over the time limit. If is true, all requests are unconditionally expired.
///
/// If true, all requests will be expired.
- public void ExpireRequests(bool expireAll = false);
+ void ExpireRequests(bool expireAll = false);
///
/// Deletes all quick connect access tokens for the provided user.
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 23230e41e..ffa19fb69 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -264,7 +264,12 @@ namespace MediaBrowser.Controller.Session
/// Task{SessionInfo}.
Task AuthenticateNewSession(AuthenticationRequest request);
- public Task AuthenticateQuickConnect(AuthenticationRequest request, string token);
+ ///
+ /// Authenticates a new session with quick connect.
+ ///
+ /// The request.
+ /// Task{SessionInfo}.
+ Task AuthenticateQuickConnect(AuthenticationRequest request, string token);
///
/// Creates the new session.
--
cgit v1.2.3
From 035d29fb357006c29ffb40e0a53c1e999237cdd1 Mon Sep 17 00:00:00 2001
From: Matt Montgomery <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Thu, 13 Aug 2020 15:35:04 -0500
Subject: Migrate to new API standard
---
.../QuickConnect/QuickConnectManager.cs | 5 +-
Jellyfin.Api/Controllers/QuickConnectController.cs | 160 +++++++++++++++++++++
Jellyfin.Api/Controllers/UserController.cs | 41 ++++++
Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs | 13 ++
.../QuickConnect/QuickConnectService.cs | 132 -----------------
.../QuickConnect/IQuickConnect.cs | 5 +-
MediaBrowser.Controller/Session/ISessionManager.cs | 1 +
7 files changed, 219 insertions(+), 138 deletions(-)
create mode 100644 Jellyfin.Api/Controllers/QuickConnectController.cs
create mode 100644 Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs
delete mode 100644 MediaBrowser.Api/QuickConnect/QuickConnectService.cs
(limited to 'MediaBrowser.Controller')
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
index 23e94afd7..949c3b505 100644
--- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
+++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Concurrent;
-using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
@@ -10,7 +9,7 @@ using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.QuickConnect;
-using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
using MediaBrowser.Common;
using Microsoft.Extensions.Logging;
using MediaBrowser.Common.Extensions;
@@ -163,7 +162,7 @@ namespace Emby.Server.Implementations.QuickConnect
}
///
- public bool AuthorizeRequest(IRequest request, string code)
+ public bool AuthorizeRequest(HttpRequest request, string code)
{
ExpireRequests();
AssertActive();
diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs
new file mode 100644
index 000000000..d45ea058d
--- /dev/null
+++ b/Jellyfin.Api/Controllers/QuickConnectController.cs
@@ -0,0 +1,160 @@
+using System.ComponentModel.DataAnnotations;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.QuickConnect;
+using MediaBrowser.Model.QuickConnect;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ ///
+ /// Quick connect controller.
+ ///
+ public class QuickConnectController : BaseJellyfinApiController
+ {
+ private readonly IQuickConnect _quickConnect;
+ private readonly IUserManager _userManager;
+ private readonly IAuthorizationContext _authContext;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of the interface.
+ public QuickConnectController(
+ IQuickConnect quickConnect,
+ IUserManager userManager,
+ IAuthorizationContext authContext)
+ {
+ _quickConnect = quickConnect;
+ _userManager = userManager;
+ _authContext = authContext;
+ }
+
+ ///
+ /// Gets the current quick connect state.
+ ///
+ /// Quick connect state returned.
+ /// The current .
+ [HttpGet("Status")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult GetStatus()
+ {
+ _quickConnect.ExpireRequests();
+ return Ok(_quickConnect.State);
+ }
+
+ ///
+ /// Initiate a new quick connect request.
+ ///
+ /// Device friendly name.
+ /// Quick connect request successfully created.
+ /// Quick connect is not active on this server.
+ /// A with a secret and code for future use or an error message.
+ [HttpGet("Initiate")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult Initiate([FromQuery] string? friendlyName)
+ {
+ return Ok(_quickConnect.TryConnect(friendlyName));
+ }
+
+ ///
+ /// Attempts to retrieve authentication information.
+ ///
+ /// Secret previously returned from the Initiate endpoint.
+ /// Quick connect result returned.
+ /// Unknown quick connect secret.
+ /// An updated .
+ [HttpGet("Connect")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult Connect([FromQuery] string? secret)
+ {
+ try
+ {
+ var result = _quickConnect.CheckRequestStatus(secret);
+ return Ok(result);
+ }
+ catch (ResourceNotFoundException)
+ {
+ return NotFound("Unknown secret");
+ }
+ }
+
+ ///
+ /// Temporarily activates quick connect for five minutes.
+ ///
+ /// Quick connect has been temporarily activated.
+ /// Quick connect is unavailable on this server.
+ /// An on success.
+ [HttpPost("Activate")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public ActionResult Activate()
+ {
+ if (_quickConnect.State == QuickConnectState.Unavailable)
+ {
+ return Forbid("Quick connect is unavailable");
+ }
+
+ _quickConnect.Activate();
+ return NoContent();
+ }
+
+ ///
+ /// Enables or disables quick connect.
+ ///
+ /// New .
+ /// Quick connect state set successfully.
+ /// An on success.
+ [HttpPost("Available")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult Available([FromQuery] QuickConnectState? status)
+ {
+ _quickConnect.SetState(status ?? QuickConnectState.Available);
+ return NoContent();
+ }
+
+ ///
+ /// Authorizes a pending quick connect request.
+ ///
+ /// Quick connect code to authorize.
+ /// Quick connect result authorized successfully.
+ /// Missing quick connect code.
+ /// Boolean indicating if the authorization was successful.
+ [HttpPost("Authorize")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public ActionResult Authorize([FromQuery, Required] string? code)
+ {
+ if (code == null)
+ {
+ return BadRequest("Missing code");
+ }
+
+ return Ok(_quickConnect.AuthorizeRequest(Request, code));
+ }
+
+ ///
+ /// Deauthorize all quick connect devices for the current user.
+ ///
+ /// All quick connect devices were deleted.
+ /// The number of devices that were deleted.
+ [HttpPost("Deauthorize")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult Deauthorize()
+ {
+ var userId = _authContext.GetAuthorizationInfo(Request).UserId;
+ return _quickConnect.DeleteAllDevices(userId);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 272312522..131fffb7a 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -216,6 +216,47 @@ namespace Jellyfin.Api.Controllers
}
}
+ ///
+ /// Authenticates a user with quick connect.
+ ///
+ /// The request.
+ /// User authenticated.
+ /// Missing token.
+ /// A containing an with information about the new session.
+ [HttpPost("AuthenticateWithQuickConnect")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request)
+ {
+ if (request.Token == null)
+ {
+ return BadRequest("Access token is required.");
+ }
+
+ var auth = _authContext.GetAuthorizationInfo(Request);
+
+ try
+ {
+ var authRequest = new AuthenticationRequest
+ {
+ App = auth.Client,
+ AppVersion = auth.Version,
+ DeviceId = auth.DeviceId,
+ DeviceName = auth.Device,
+ };
+
+ var result = await _sessionManager.AuthenticateQuickConnect(
+ authRequest,
+ request.Token).ConfigureAwait(false);
+
+ return result;
+ }
+ catch (SecurityException e)
+ {
+ // rethrow adding IP address to message
+ throw new SecurityException($"[{HttpContext.Connection.RemoteIpAddress}] {e.Message}", e);
+ }
+ }
+
///
/// Updates a user's password.
///
diff --git a/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs b/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs
new file mode 100644
index 000000000..8f53d5f37
--- /dev/null
+++ b/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs
@@ -0,0 +1,13 @@
+namespace Jellyfin.Api.Models.UserDtos
+{
+ ///
+ /// The quick connect request body.
+ ///
+ public class QuickConnectDto
+ {
+ ///
+ /// Gets or sets the quick connect token.
+ ///
+ public string? Token { get; set; }
+ }
+}
diff --git a/MediaBrowser.Api/QuickConnect/QuickConnectService.cs b/MediaBrowser.Api/QuickConnect/QuickConnectService.cs
deleted file mode 100644
index 7093be990..000000000
--- a/MediaBrowser.Api/QuickConnect/QuickConnectService.cs
+++ /dev/null
@@ -1,132 +0,0 @@
-using System;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.QuickConnect;
-using MediaBrowser.Model.QuickConnect;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.QuickConnect
-{
- [Route("/QuickConnect/Initiate", "GET", Summary = "Requests a new quick connect code")]
- public class Initiate : IReturn
- {
- [ApiMember(Name = "FriendlyName", Description = "Device friendly name", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string FriendlyName { get; set; }
- }
-
- [Route("/QuickConnect/Connect", "GET", Summary = "Attempts to retrieve authentication information")]
- public class Connect : IReturn
- {
- [ApiMember(Name = "Secret", Description = "Quick connect secret", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Secret { get; set; }
- }
-
- [Route("/QuickConnect/Authorize", "POST", Summary = "Authorizes a pending quick connect request")]
- [Authenticated]
- public class Authorize : IReturn
- {
- [ApiMember(Name = "Code", Description = "Quick connect identifying code", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Code { get; set; }
- }
-
- [Route("/QuickConnect/Deauthorize", "POST", Summary = "Deletes all quick connect authorization tokens for the current user")]
- [Authenticated]
- public class Deauthorize : IReturn
- {
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
- }
-
- [Route("/QuickConnect/Status", "GET", Summary = "Gets the current quick connect state")]
- public class QuickConnectStatus : IReturn
- {
-
- }
-
- [Route("/QuickConnect/Available", "POST", Summary = "Enables or disables quick connect")]
- [Authenticated(Roles = "Admin")]
- public class Available : IReturn
- {
- [ApiMember(Name = "Status", Description = "New quick connect status", IsRequired = false, DataType = "QuickConnectState", ParameterType = "query", Verb = "GET")]
- public QuickConnectState Status { get; set; }
- }
-
- [Route("/QuickConnect/Activate", "POST", Summary = "Temporarily activates quick connect for the time period defined in the server configuration")]
- [Authenticated]
- public class Activate : IReturn
- {
-
- }
-
- public class QuickConnectService : BaseApiService
- {
- private IQuickConnect _quickConnect;
- private IUserManager _userManager;
- private IAuthorizationContext _authContext;
-
- public QuickConnectService(
- ILogger logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IUserManager userManager,
- IAuthorizationContext authContext,
- IQuickConnect quickConnect)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _userManager = userManager;
- _quickConnect = quickConnect;
- _authContext = authContext;
- }
-
- public object Get(Initiate request)
- {
- return _quickConnect.TryConnect(request.FriendlyName);
- }
-
- public object Get(Connect request)
- {
- return _quickConnect.CheckRequestStatus(request.Secret);
- }
-
- public object Get(QuickConnectStatus request)
- {
- _quickConnect.ExpireRequests();
- return _quickConnect.State;
- }
-
- public object Post(Deauthorize request)
- {
- AssertCanUpdateUser(_authContext, _userManager, request.UserId, true);
-
- return _quickConnect.DeleteAllDevices(request.UserId);
- }
-
- public object Post(Authorize request)
- {
- return _quickConnect.AuthorizeRequest(Request, request.Code);
- }
-
- public object Post(Activate request)
- {
- if (_quickConnect.State == QuickConnectState.Unavailable)
- {
- return false;
- }
-
- string name = _authContext.GetAuthorizationInfo(Request).User.Username;
-
- Logger.LogInformation("{name} temporarily activated quick connect", name);
- _quickConnect.Activate();
-
- return true;
- }
-
- public object Post(Available request)
- {
- _quickConnect.SetState(request.Status);
- return _quickConnect.State;
- }
- }
-}
diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
index 993637c8a..fd7e973f6 100644
--- a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
+++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
@@ -1,7 +1,6 @@
using System;
-using System.Collections.Generic;
using MediaBrowser.Model.QuickConnect;
-using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.QuickConnect
{
@@ -66,7 +65,7 @@ namespace MediaBrowser.Controller.QuickConnect
/// HTTP request object.
/// Identifying code for the request.
/// A boolean indicating if the authorization completed successfully.
- bool AuthorizeRequest(IRequest request, string code);
+ bool AuthorizeRequest(HttpRequest request, string code);
///
/// Expire quick connect requests that are over the time limit. If is true, all requests are unconditionally expired.
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index ffa19fb69..d44787b88 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -268,6 +268,7 @@ namespace MediaBrowser.Controller.Session
/// Authenticates a new session with quick connect.
///
/// The request.
+ /// Quick connect access token.
/// Task{SessionInfo}.
Task AuthenticateQuickConnect(AuthenticationRequest request, string token);
--
cgit v1.2.3
From 5f1a86324170387f12602d77dad7249faf30548f Mon Sep 17 00:00:00 2001
From: Matt Montgomery <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Mon, 17 Aug 2020 16:36:45 -0500
Subject: Apply suggestions from code review
---
.../QuickConnect/QuickConnectManager.cs | 38 +++++++++-------------
.../Session/SessionManager.cs | 2 +-
Jellyfin.Api/Controllers/QuickConnectController.cs | 34 +++++++++----------
Jellyfin.Api/Controllers/UserController.cs | 4 +--
.../QuickConnect/IQuickConnect.cs | 12 +++----
.../QuickConnect/QuickConnectResult.cs | 5 ---
6 files changed, 40 insertions(+), 55 deletions(-)
(limited to 'MediaBrowser.Controller')
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
index 949c3b505..52e934229 100644
--- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
+++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
@@ -3,17 +3,16 @@ using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
+using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.QuickConnect;
-using Microsoft.AspNetCore.Http;
-using MediaBrowser.Common;
using Microsoft.Extensions.Logging;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Authentication;
namespace Emby.Server.Implementations.QuickConnect
{
@@ -60,7 +59,7 @@ namespace Emby.Server.Implementations.QuickConnect
public int CodeLength { get; set; } = 6;
///
- public string TokenNamePrefix { get; set; } = "QuickConnect-";
+ public string TokenName { get; set; } = "QuickConnect";
///
public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable;
@@ -82,7 +81,7 @@ namespace Emby.Server.Implementations.QuickConnect
///
public void Activate()
{
- DateActivated = DateTime.Now;
+ DateActivated = DateTime.UtcNow;
SetState(QuickConnectState.Active);
}
@@ -101,7 +100,7 @@ namespace Emby.Server.Implementations.QuickConnect
}
///
- public QuickConnectResult TryConnect(string friendlyName)
+ public QuickConnectResult TryConnect()
{
ExpireRequests();
@@ -111,14 +110,11 @@ namespace Emby.Server.Implementations.QuickConnect
throw new AuthenticationException("Quick connect is not active on this server");
}
- _logger.LogDebug("Got new quick connect request from {friendlyName}", friendlyName);
-
var code = GenerateCode();
var result = new QuickConnectResult()
{
Secret = GenerateSecureRandom(),
- FriendlyName = friendlyName,
- DateAdded = DateTime.Now,
+ DateAdded = DateTime.UtcNow,
Code = code
};
@@ -162,13 +158,11 @@ namespace Emby.Server.Implementations.QuickConnect
}
///
- public bool AuthorizeRequest(HttpRequest request, string code)
+ public bool AuthorizeRequest(Guid userId, string code)
{
ExpireRequests();
AssertActive();
- var auth = _authContext.GetAuthorizationInfo(request);
-
if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
{
throw new ResourceNotFoundException("Unable to find request");
@@ -182,21 +176,21 @@ namespace Emby.Server.Implementations.QuickConnect
result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
// Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated.
- var added = result.DateAdded ?? DateTime.Now.Subtract(new TimeSpan(0, Timeout, 0));
- result.DateAdded = added.Subtract(new TimeSpan(0, Timeout - 1, 0));
+ var added = result.DateAdded ?? DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(Timeout));
+ result.DateAdded = added.Subtract(TimeSpan.FromMinutes(Timeout - 1));
_authenticationRepository.Create(new AuthenticationInfo
{
- AppName = TokenNamePrefix + result.FriendlyName,
+ AppName = TokenName,
AccessToken = result.Authentication,
DateCreated = DateTime.UtcNow,
DeviceId = _appHost.SystemId,
DeviceName = _appHost.FriendlyName,
AppVersion = _appHost.ApplicationVersionString,
- UserId = auth.UserId
+ UserId = userId
});
- _logger.LogInformation("Allowing device {FriendlyName} to login as user {Username} with quick connect code {Code}", result.FriendlyName, auth.User.Username, result.Code);
+ _logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId);
return true;
}
@@ -210,7 +204,7 @@ namespace Emby.Server.Implementations.QuickConnect
UserId = user
});
- var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenNamePrefix, StringComparison.CurrentCulture));
+ var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenName, StringComparison.CurrentCulture));
var removed = 0;
foreach (var token in tokens)
@@ -256,7 +250,7 @@ namespace Emby.Server.Implementations.QuickConnect
public void ExpireRequests(bool expireAll = false)
{
// Check if quick connect should be deactivated
- if (State == QuickConnectState.Active && DateTime.Now > DateActivated.AddMinutes(Timeout) && !expireAll)
+ if (State == QuickConnectState.Active && DateTime.UtcNow > DateActivated.AddMinutes(Timeout) && !expireAll)
{
_logger.LogDebug("Quick connect time expired, deactivating");
SetState(QuickConnectState.Available);
@@ -270,7 +264,7 @@ namespace Emby.Server.Implementations.QuickConnect
for (int i = 0; i < values.Count; i++)
{
var added = values[i].DateAdded ?? DateTime.UnixEpoch;
- if (DateTime.Now > added.AddMinutes(Timeout) || expireAll)
+ if (DateTime.UtcNow > added.AddMinutes(Timeout) || expireAll)
{
code = values[i].Code;
_logger.LogDebug("Removing expired request {code}", code);
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 8a8223ee7..fbe8e065c 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -1433,7 +1433,7 @@ namespace Emby.Server.Implementations.Session
Limit = 1
});
- if (result.TotalRecordCount < 1)
+ if (result.TotalRecordCount == 0)
{
throw new SecurityException("Unknown quick connect token");
}
diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs
index 1625bcffe..b1ee2ff53 100644
--- a/Jellyfin.Api/Controllers/QuickConnectController.cs
+++ b/Jellyfin.Api/Controllers/QuickConnectController.cs
@@ -1,8 +1,8 @@
+using System;
using System.ComponentModel.DataAnnotations;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Model.QuickConnect;
@@ -18,22 +18,18 @@ namespace Jellyfin.Api.Controllers
public class QuickConnectController : BaseJellyfinApiController
{
private readonly IQuickConnect _quickConnect;
- private readonly IUserManager _userManager;
private readonly IAuthorizationContext _authContext;
///
/// Initializes a new instance of the class.
///
/// Instance of the interface.
- /// Instance of the interface.
/// Instance of the interface.
public QuickConnectController(
IQuickConnect quickConnect,
- IUserManager userManager,
IAuthorizationContext authContext)
{
_quickConnect = quickConnect;
- _userManager = userManager;
_authContext = authContext;
}
@@ -53,15 +49,14 @@ namespace Jellyfin.Api.Controllers
///
/// Initiate a new quick connect request.
///
- /// Device friendly name.
/// Quick connect request successfully created.
/// Quick connect is not active on this server.
/// A with a secret and code for future use or an error message.
[HttpGet("Initiate")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult Initiate([FromQuery] string? friendlyName)
+ public ActionResult Initiate()
{
- return _quickConnect.TryConnect(friendlyName);
+ return _quickConnect.TryConnect();
}
///
@@ -74,12 +69,11 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Connect")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult Connect([FromQuery] string? secret)
+ public ActionResult Connect([FromQuery, Required] string secret)
{
try
{
- var result = _quickConnect.CheckRequestStatus(secret);
- return result;
+ return _quickConnect.CheckRequestStatus(secret);
}
catch (ResourceNotFoundException)
{
@@ -117,9 +111,9 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Available")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult Available([FromQuery] QuickConnectState? status)
+ public ActionResult Available([FromQuery] QuickConnectState status = QuickConnectState.Available)
{
- _quickConnect.SetState(status ?? QuickConnectState.Available);
+ _quickConnect.SetState(status);
return NoContent();
}
@@ -127,16 +121,22 @@ namespace Jellyfin.Api.Controllers
/// Authorizes a pending quick connect request.
///
/// Quick connect code to authorize.
+ /// User id.
/// Quick connect result authorized successfully.
- /// Missing quick connect code.
+ /// User is not allowed to authorize quick connect requests.
/// Boolean indicating if the authorization was successful.
[HttpPost("Authorize")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- public ActionResult Authorize([FromQuery, Required] string? code)
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public ActionResult Authorize([FromQuery, Required] string code, [FromQuery, Required] Guid userId)
{
- return _quickConnect.AuthorizeRequest(Request, code);
+ if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
+ {
+ return Forbid("User is not allowed to authorize quick connect requests.");
+ }
+
+ return _quickConnect.AuthorizeRequest(userId, code);
}
///
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 355816bd3..d67f82219 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -239,11 +239,9 @@ namespace Jellyfin.Api.Controllers
DeviceName = auth.Device,
};
- var result = await _sessionManager.AuthenticateQuickConnect(
+ return await _sessionManager.AuthenticateQuickConnect(
authRequest,
request.Token).ConfigureAwait(false);
-
- return result;
}
catch (SecurityException e)
{
diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
index fd7e973f6..959a2d771 100644
--- a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
+++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
@@ -1,6 +1,5 @@
using System;
using MediaBrowser.Model.QuickConnect;
-using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.QuickConnect
{
@@ -15,9 +14,9 @@ namespace MediaBrowser.Controller.QuickConnect
int CodeLength { get; set; }
///
- /// Gets or sets the string to prefix internal access tokens with.
+ /// Gets or sets the name of internal access tokens.
///
- string TokenNamePrefix { get; set; }
+ string TokenName { get; set; }
///
/// Gets the current state of quick connect.
@@ -48,9 +47,8 @@ namespace MediaBrowser.Controller.QuickConnect
///
/// Initiates a new quick connect request.
///
- /// Friendly device name to display in the request UI.
/// A quick connect result with tokens to proceed or throws an exception if not active.
- QuickConnectResult TryConnect(string friendlyName);
+ QuickConnectResult TryConnect();
///
/// Checks the status of an individual request.
@@ -62,10 +60,10 @@ namespace MediaBrowser.Controller.QuickConnect
///
/// Authorizes a quick connect request to connect as the calling user.
///
- /// HTTP request object.
+ /// User id.
/// Identifying code for the request.
/// A boolean indicating if the authorization completed successfully.
- bool AuthorizeRequest(HttpRequest request, string code);
+ bool AuthorizeRequest(Guid userId, string code);
///
/// Expire quick connect requests that are over the time limit. If is true, all requests are unconditionally expired.
diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs
index a10d60d57..0fa40b6a7 100644
--- a/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs
+++ b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs
@@ -22,11 +22,6 @@ namespace MediaBrowser.Model.QuickConnect
///
public string? Code { get; set; }
- ///
- /// Gets or sets the device friendly name.
- ///
- public string? FriendlyName { get; set; }
-
///
/// Gets or sets the private access token.
///
--
cgit v1.2.3
From e3377564288598742dbf64f396ed38e42b6b2915 Mon Sep 17 00:00:00 2001
From: Claus Vium
Date: Wed, 2 Sep 2020 12:22:14 +0200
Subject: Remove ServiceStack and related stuff
---
Emby.Server.Implementations/ApplicationHost.cs | 8 +-
.../HttpServer/FileWriter.cs | 250 -------
.../HttpServer/HttpListenerHost.cs | 233 +------
.../HttpServer/HttpResultFactory.cs | 721 ---------------------
.../HttpServer/RangeRequestWriter.cs | 212 ------
.../HttpServer/ResponseFilter.cs | 113 ----
.../HttpServer/Security/AuthService.cs | 213 +-----
.../HttpServer/Security/AuthorizationContext.cs | 21 +-
.../HttpServer/Security/SessionContext.cs | 20 +-
.../HttpServer/StreamWriter.cs | 120 ----
Emby.Server.Implementations/Services/HttpResult.cs | 64 --
.../Services/RequestHelper.cs | 51 --
.../Services/ResponseHelper.cs | 141 ----
.../Services/ServiceController.cs | 202 ------
.../Services/ServiceExec.cs | 230 -------
.../Services/ServiceHandler.cs | 212 ------
.../Services/ServiceMethod.cs | 20 -
.../Services/ServicePath.cs | 550 ----------------
.../Services/StringMapTypeDeserializer.cs | 118 ----
.../Services/UrlExtensions.cs | 27 -
.../SocketSharp/WebSocketSharpRequest.cs | 248 -------
.../Extensions/HttpContextExtensions.cs | 55 +-
.../MediaEncoding/EncodingJobOptions.cs | 30 -
.../Net/AuthenticatedAttribute.cs | 76 ---
MediaBrowser.Controller/Net/IAuthService.cs | 17 -
.../Net/IAuthorizationContext.cs | 3 +-
MediaBrowser.Controller/Net/IHasResultFactory.cs | 17 -
MediaBrowser.Controller/Net/IHttpResultFactory.cs | 82 ---
MediaBrowser.Controller/Net/IHttpServer.cs | 9 +-
MediaBrowser.Controller/Net/ISessionContext.cs | 6 +-
MediaBrowser.Controller/Net/StaticResultOptions.cs | 44 --
MediaBrowser.Model/Services/ApiMemberAttribute.cs | 65 --
MediaBrowser.Model/Services/IAsyncStreamWriter.cs | 13 -
MediaBrowser.Model/Services/IHasHeaders.cs | 11 -
MediaBrowser.Model/Services/IHasRequestFilter.cs | 24 -
MediaBrowser.Model/Services/IHttpRequest.cs | 17 -
MediaBrowser.Model/Services/IHttpResult.cs | 35 -
MediaBrowser.Model/Services/IRequest.cs | 93 ---
.../Services/IRequiresRequestStream.cs | 14 -
MediaBrowser.Model/Services/IService.cs | 15 -
MediaBrowser.Model/Services/IStreamWriter.cs | 11 -
.../Services/QueryParamCollection.cs | 147 -----
MediaBrowser.Model/Services/RouteAttribute.cs | 163 -----
MediaBrowser.Model/Session/PlayRequest.cs | 1 -
.../HttpServer/ResponseFilterTests.cs | 18 -
45 files changed, 91 insertions(+), 4649 deletions(-)
delete mode 100644 Emby.Server.Implementations/HttpServer/FileWriter.cs
delete mode 100644 Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
delete mode 100644 Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs
delete mode 100644 Emby.Server.Implementations/HttpServer/ResponseFilter.cs
delete mode 100644 Emby.Server.Implementations/HttpServer/StreamWriter.cs
delete mode 100644 Emby.Server.Implementations/Services/HttpResult.cs
delete mode 100644 Emby.Server.Implementations/Services/RequestHelper.cs
delete mode 100644 Emby.Server.Implementations/Services/ResponseHelper.cs
delete mode 100644 Emby.Server.Implementations/Services/ServiceController.cs
delete mode 100644 Emby.Server.Implementations/Services/ServiceExec.cs
delete mode 100644 Emby.Server.Implementations/Services/ServiceHandler.cs
delete mode 100644 Emby.Server.Implementations/Services/ServiceMethod.cs
delete mode 100644 Emby.Server.Implementations/Services/ServicePath.cs
delete mode 100644 Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
delete mode 100644 Emby.Server.Implementations/Services/UrlExtensions.cs
delete mode 100644 Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
delete mode 100644 MediaBrowser.Controller/Net/AuthenticatedAttribute.cs
delete mode 100644 MediaBrowser.Controller/Net/IHasResultFactory.cs
delete mode 100644 MediaBrowser.Controller/Net/IHttpResultFactory.cs
delete mode 100644 MediaBrowser.Controller/Net/StaticResultOptions.cs
delete mode 100644 MediaBrowser.Model/Services/ApiMemberAttribute.cs
delete mode 100644 MediaBrowser.Model/Services/IAsyncStreamWriter.cs
delete mode 100644 MediaBrowser.Model/Services/IHasHeaders.cs
delete mode 100644 MediaBrowser.Model/Services/IHasRequestFilter.cs
delete mode 100644 MediaBrowser.Model/Services/IHttpRequest.cs
delete mode 100644 MediaBrowser.Model/Services/IHttpResult.cs
delete mode 100644 MediaBrowser.Model/Services/IRequest.cs
delete mode 100644 MediaBrowser.Model/Services/IRequiresRequestStream.cs
delete mode 100644 MediaBrowser.Model/Services/IService.cs
delete mode 100644 MediaBrowser.Model/Services/IStreamWriter.cs
delete mode 100644 MediaBrowser.Model/Services/QueryParamCollection.cs
delete mode 100644 MediaBrowser.Model/Services/RouteAttribute.cs
delete mode 100644 tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs
(limited to 'MediaBrowser.Controller')
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index e9b063277..4f47d1999 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -41,7 +41,6 @@ using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization;
-using Emby.Server.Implementations.Services;
using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
@@ -90,7 +89,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Chapters;
@@ -544,8 +542,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
-
ServiceCollection.AddSingleton(this);
ServiceCollection.AddSingleton(ApplicationPaths);
@@ -581,7 +577,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
ServiceCollection.AddSingleton();
ServiceCollection.AddSingleton();
@@ -757,7 +752,6 @@ namespace Emby.Server.Implementations
CollectionFolder.XmlSerializer = _xmlSerializer;
CollectionFolder.JsonSerializer = Resolve();
CollectionFolder.ApplicationHost = this;
- AuthenticatedAttribute.AuthService = Resolve();
}
///
@@ -777,7 +771,7 @@ namespace Emby.Server.Implementations
.Where(i => i != null)
.ToArray();
- _httpServer.Init(GetExportTypes(), GetExports(), GetUrlPrefixes());
+ _httpServer.Init(GetExports(), GetUrlPrefixes());
Resolve().AddParts(
GetExports(),
diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs
deleted file mode 100644
index 6fce8de44..000000000
--- a/Emby.Server.Implementations/HttpServer/FileWriter.cs
+++ /dev/null
@@ -1,250 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Runtime.InteropServices;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpServer
-{
- public class FileWriter : IHttpResult
- {
- private static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
-
- private static readonly string[] _skipLogExtensions = {
- ".js",
- ".html",
- ".css"
- };
-
- private readonly IStreamHelper _streamHelper;
- private readonly ILogger _logger;
-
- ///
- /// The _options.
- ///
- private readonly IDictionary _options = new Dictionary();
-
- ///
- /// The _requested ranges.
- ///
- private List> _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();
- }
-
- 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 Cookies { get; private set; }
-
- public FileShare FileShare { get; set; }
-
- ///
- /// Gets the options.
- ///
- /// The options.
- public IDictionary Headers => _options;
-
- public string Path { get; set; }
-
- ///
- /// Gets the requested ranges.
- ///
- /// The requested ranges.
- protected List> RequestedRanges
- {
- get
- {
- if (_requestedRanges == null)
- {
- _requestedRanges = new List>();
-
- // 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(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;
- }
-
- ///
- /// Sets the range values.
- ///
- private void SetRangeValues()
- {
- var requestedRange = RequestedRanges[0];
-
- // If the requested range is "0-", we can optimize by just doing a stream copy
- if (!requestedRange.Value.HasValue)
- {
- RangeEnd = TotalContentLength - 1;
- }
- else
- {
- RangeEnd = requestedRange.Value.Value;
- }
-
- RangeStart = requestedRange.Key;
- RangeLength = 1 + RangeEnd - RangeStart;
-
- // Content-Length is the length of what we're serving, not the original content
- var lengthString = RangeLength.ToString(CultureInfo.InvariantCulture);
- Headers[HeaderNames.ContentLength] = lengthString;
- var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
- Headers[HeaderNames.ContentRange] = rangeString;
-
- _logger.LogDebug("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString);
- }
-
- public async Task WriteToAsync(HttpResponse response, CancellationToken cancellationToken)
- {
- try
- {
- // Headers only
- if (IsHeadRequest)
- {
- return;
- }
-
- var path = Path;
- var offset = RangeStart;
- var count = RangeLength;
-
- if (string.IsNullOrWhiteSpace(RangeHeader) || RangeStart <= 0 && RangeEnd >= TotalContentLength - 1)
- {
- var extension = System.IO.Path.GetExtension(path);
-
- if (extension == null || !_skipLogExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
- {
- _logger.LogDebug("Transmit file {0}", path);
- }
-
- offset = 0;
- count = 0;
- }
-
- await TransmitFile(response.Body, path, offset, count, FileShare, cancellationToken).ConfigureAwait(false);
- }
- finally
- {
- OnComplete?.Invoke();
- }
- }
-
- public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShare fileShare, CancellationToken cancellationToken)
- {
- var fileOptions = FileOptions.SequentialScan;
-
- // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- fileOptions |= FileOptions.Asynchronous;
- }
-
- using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, fileOptions))
- {
- if (offset > 0)
- {
- fs.Position = offset;
- }
-
- if (count > 0)
- {
- await _streamHelper.CopyToAsync(fs, stream, count, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- await fs.CopyToAsync(stream, IODefaults.CopyToBufferSize, cancellationToken).ConfigureAwait(false);
- }
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
index fe39bb4b2..30cb7dd3a 100644
--- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
+++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
@@ -7,11 +7,8 @@ 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 Jellyfin.Data.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
@@ -20,8 +17,6 @@ using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
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;
@@ -29,7 +24,6 @@ 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
{
@@ -46,13 +40,9 @@ namespace Emby.Server.Implementations.HttpServer
private readonly IServerConfigurationManager _config;
private readonly INetworkManager _networkManager;
private readonly IServerApplicationHost _appHost;
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IXmlSerializer _xmlSerializer;
- private readonly Func> _funcParseFn;
private readonly string _defaultRedirectPath;
private readonly string _baseUrlPrefix;
- private readonly Dictionary _serviceOperationsMap = new Dictionary();
private readonly IHostEnvironment _hostEnvironment;
private IWebSocketListener[] _webSocketListeners = Array.Empty();
@@ -64,10 +54,7 @@ namespace Emby.Server.Implementations.HttpServer
IServerConfigurationManager config,
IConfiguration configuration,
INetworkManager networkManager,
- IJsonSerializer jsonSerializer,
- IXmlSerializer xmlSerializer,
ILocalizationManager localizationManager,
- ServiceController serviceController,
IHostEnvironment hostEnvironment,
ILoggerFactory loggerFactory)
{
@@ -77,36 +64,21 @@ namespace Emby.Server.Implementations.HttpServer
_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>();
GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
}
public event EventHandler> WebSocketConnected;
- public Action[] 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] == '/')
@@ -121,58 +93,6 @@ namespace Emby.Server.Implementations.HttpServer
}
}
- ///
- /// Applies the request filters. Returns whether or not the request has been handled
- /// and no more processing should be done.
- ///
- ///
- 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 GetRequestFilterAttributes(Type requestDtoType)
- {
- var attributes = requestDtoType.GetCustomAttributes(true).OfType().ToList();
-
- var serviceType = GetServiceTypeByRequest(requestDtoType);
- if (serviceType != null)
- {
- attributes.AddRange(serviceType.GetCustomAttributes(true).OfType());
- }
-
- attributes.Sort((x, y) => x.Priority - y.Priority);
-
- return attributes;
- }
-
private static Exception GetActualException(Exception ex)
{
if (ex is AggregateException agg)
@@ -210,7 +130,7 @@ namespace Emby.Server.Implementations.HttpServer
}
}
- private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace)
+ private async Task ErrorHandler(Exception ex, HttpContext httpContext, int statusCode, string urlToLog, bool ignoreStackTrace)
{
if (ignoreStackTrace)
{
@@ -221,7 +141,7 @@ namespace Emby.Server.Implementations.HttpServer
_logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog);
}
- var httpRes = httpReq.Response;
+ var httpRes = httpContext.Response;
if (httpRes.HasStarted)
{
@@ -395,24 +315,22 @@ namespace Emby.Server.Implementations.HttpServer
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);
+ return RequestHandler(context, context.RequestAborted);
}
///
/// Overridable method that can be used to implement a custom handler.
///
- private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
+ private async Task RequestHandler(HttpContext httpContext, CancellationToken cancellationToken)
{
var stopWatch = new Stopwatch();
stopWatch.Start();
- var httpRes = httpReq.Response;
+ var httpRes = httpContext.Response;
+ var host = httpContext.Request.Host.ToString();
+ var localPath = httpContext.Request.Path.ToString();
+ var urlString = httpContext.Request.GetDisplayUrl();
string urlToLog = GetUrlToLog(urlString);
- string remoteIp = httpReq.RemoteIp;
+ string remoteIp = httpContext.Request.RemoteIp();
try
{
@@ -432,7 +350,7 @@ namespace Emby.Server.Implementations.HttpServer
return;
}
- if (!ValidateRequest(remoteIp, httpReq.IsLocal))
+ if (!ValidateRequest(remoteIp, httpContext.Request.IsLocal()))
{
httpRes.StatusCode = 403;
httpRes.ContentType = "text/plain";
@@ -440,16 +358,16 @@ namespace Emby.Server.Implementations.HttpServer
return;
}
- if (!ValidateSsl(httpReq.RemoteIp, urlString))
+ if (!ValidateSsl(httpContext.Request.RemoteIp(), urlString))
{
- RedirectToSecureUrl(httpReq, httpRes, urlString);
+ RedirectToSecureUrl(httpRes, urlString);
return;
}
- if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(httpContext.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase))
{
httpRes.StatusCode = 200;
- foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
+ foreach (var (key, value) in GetDefaultCorsHeaders(httpContext))
{
httpRes.Headers.Add(key, value);
}
@@ -483,15 +401,7 @@ namespace Emby.Server.Implementations.HttpServer
}
}
- var handler = GetServiceHandler(httpReq);
- if (handler != null)
- {
- await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- throw new FileNotFoundException();
- }
+ throw new FileNotFoundException();
}
catch (Exception requestEx)
{
@@ -500,7 +410,7 @@ namespace Emby.Server.Implementations.HttpServer
var requestInnerEx = GetActualException(requestEx);
var statusCode = GetStatusCode(requestInnerEx);
- foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
+ foreach (var (key, value) in GetDefaultCorsHeaders(httpContext))
{
if (!httpRes.Headers.ContainsKey(key))
{
@@ -525,7 +435,7 @@ namespace Emby.Server.Implementations.HttpServer
throw;
}
- await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false);
+ await ErrorHandler(requestInnerEx, httpContext, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false);
}
catch (Exception handlerException)
{
@@ -596,12 +506,12 @@ namespace Emby.Server.Implementations.HttpServer
///
///
///
- public IDictionary GetDefaultCorsHeaders(IRequest req)
+ public IDictionary GetDefaultCorsHeaders(HttpContext httpContext)
{
- var origin = req.Headers["Origin"];
+ var origin = httpContext.Request.Headers["Origin"];
if (origin == StringValues.Empty)
{
- origin = req.Headers["Host"];
+ origin = httpContext.Request.Headers["Host"];
if (origin == StringValues.Empty)
{
origin = "*";
@@ -616,23 +526,7 @@ namespace Emby.Server.Implementations.HttpServer
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)
+ private void RedirectToSecureUrl(HttpResponse httpRes, string url)
{
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
{
@@ -650,95 +544,12 @@ namespace Emby.Server.Implementations.HttpServer
///
/// Adds the rest handlers.
///
- /// The service types to register with the .
/// The web socket listeners.
/// The URL prefixes. See .
- public void Init(IEnumerable serviceTypes, IEnumerable listeners, IEnumerable urlPrefixes)
+ public void Init(IEnumerable listeners, IEnumerable urlPrefixes)
{
_webSocketListeners = listeners.ToArray();
UrlPrefixes = urlPrefixes.ToArray();
-
- ServiceController.Init(this, serviceTypes);
-
- ResponseFilters = new Action[]
- {
- new ResponseFilter(this, _logger).FilterResponse
- };
- }
-
- public RouteAttribute[] GetRouteAttributes(Type requestType)
- {
- var routes = requestType.GetTypeInfo().GetCustomAttributes(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 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
/// The HTTP req.
/// Dictionary{System.StringSystem.String}.
- private Dictionary GetAuthorizationDictionary(IRequest httpReq)
+ private Dictionary GetAuthorizationDictionary(HttpContext httpReq)
{
- var auth = httpReq.Headers["X-Emby-Authorization"];
+ var auth = httpReq.Request.Headers["X-Emby-Authorization"];
if (string.IsNullOrEmpty(auth))
{
- auth = httpReq.Headers[HeaderNames.Authorization];
+ auth = httpReq.Request.Headers[HeaderNames.Authorization];
}
return GetAuthorization(auth);
diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
index 03fcfa53d..8777c59b7 100644
--- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
@@ -2,11 +2,11 @@
using System;
using Jellyfin.Data.Entities;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
namespace Emby.Server.Implementations.HttpServer.Security
{
@@ -23,26 +23,20 @@ namespace Emby.Server.Implementations.HttpServer.Security
_sessionManager = sessionManager;
}
- public SessionInfo GetSession(IRequest requestContext)
+ public SessionInfo GetSession(HttpContext requestContext)
{
var authorization = _authContext.GetAuthorizationInfo(requestContext);
var user = authorization.User;
- return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.RemoteIp, user);
- }
-
- private AuthenticationInfo GetTokenInfo(IRequest request)
- {
- request.Items.TryGetValue("OriginalAuthenticationInfo", out var info);
- return info as AuthenticationInfo;
+ return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.Request.RemoteIp(), user);
}
public SessionInfo GetSession(object requestContext)
{
- return GetSession((IRequest)requestContext);
+ return GetSession((HttpContext)requestContext);
}
- public User GetUser(IRequest requestContext)
+ public User GetUser(HttpContext requestContext)
{
var session = GetSession(requestContext);
@@ -51,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
public User GetUser(object requestContext)
{
- return GetUser((IRequest)requestContext);
+ return GetUser((HttpContext)requestContext);
}
}
}
diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs
deleted file mode 100644
index 00e3ab8fe..000000000
--- a/Emby.Server.Implementations/HttpServer/StreamWriter.cs
+++ /dev/null
@@ -1,120 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Services;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpServer
-{
- ///
- /// Class StreamWriter.
- ///
- public class StreamWriter : IAsyncStreamWriter, IHasHeaders
- {
- ///
- /// The options.
- ///
- private readonly IDictionary _options = new Dictionary();
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The source.
- /// Type of the content.
- 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;
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The source.
- /// Type of the content.
- /// The content length.
- 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;
- }
-
- ///
- /// Gets or sets the source stream.
- ///
- /// The source stream.
- private Stream SourceStream { get; set; }
-
- private byte[] SourceBytes { get; set; }
-
- ///
- /// Gets the options.
- ///
- /// The options.
- public IDictionary Headers => _options;
-
- ///
- /// Fires when complete.
- ///
- public Action OnComplete { get; set; }
-
- ///
- /// Fires when an error occours.
- ///
- public Action OnError { get; set; }
-
- ///
- public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
- {
- try
- {
- var bytes = SourceBytes;
-
- if (bytes != null)
- {
- await responseStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- using (var src = SourceStream)
- {
- await src.CopyToAsync(responseStream, cancellationToken).ConfigureAwait(false);
- }
- }
- }
- catch
- {
- OnError?.Invoke();
-
- throw;
- }
- finally
- {
- OnComplete?.Invoke();
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/HttpResult.cs b/Emby.Server.Implementations/Services/HttpResult.cs
deleted file mode 100644
index 8ba86f756..000000000
--- a/Emby.Server.Implementations/Services/HttpResult.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Server.Implementations.Services
-{
- public class HttpResult
- : IHttpResult, IAsyncStreamWriter
- {
- public HttpResult(object response, string contentType, HttpStatusCode statusCode)
- {
- this.Headers = new Dictionary();
-
- this.Response = response;
- this.ContentType = contentType;
- this.StatusCode = statusCode;
- }
-
- public object Response { get; set; }
-
- public string ContentType { get; set; }
-
- public IDictionary Headers { get; private set; }
-
- public int Status { get; set; }
-
- public HttpStatusCode StatusCode
- {
- get => (HttpStatusCode)Status;
- set => Status = (int)value;
- }
-
- public IRequest RequestContext { get; set; }
-
- public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
- {
- var response = RequestContext?.Response;
-
- if (this.Response is byte[] bytesResponse)
- {
- var contentLength = bytesResponse.Length;
-
- if (response != null)
- {
- response.ContentLength = contentLength;
- }
-
- if (contentLength > 0)
- {
- await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false);
- }
-
- return;
- }
-
- await ResponseHelper.WriteObject(this.RequestContext, this.Response, response).ConfigureAwait(false);
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/RequestHelper.cs b/Emby.Server.Implementations/Services/RequestHelper.cs
deleted file mode 100644
index 1f9c7fc22..000000000
--- a/Emby.Server.Implementations/Services/RequestHelper.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.IO;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-
-namespace Emby.Server.Implementations.Services
-{
- public class RequestHelper
- {
- public static Func> GetRequestReader(HttpListenerHost host, string contentType)
- {
- switch (GetContentTypeWithoutEncoding(contentType))
- {
- case "application/xml":
- case "text/xml":
- case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
- return host.DeserializeXml;
-
- case "application/json":
- case "text/json":
- return host.DeserializeJson;
- }
-
- return null;
- }
-
- public static Action GetResponseWriter(HttpListenerHost host, string contentType)
- {
- switch (GetContentTypeWithoutEncoding(contentType))
- {
- case "application/xml":
- case "text/xml":
- case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
- return host.SerializeToXml;
-
- case "application/json":
- case "text/json":
- return host.SerializeToJson;
- }
-
- return null;
- }
-
- private static string GetContentTypeWithoutEncoding(string contentType)
- {
- return contentType?.Split(';')[0].ToLowerInvariant().Trim();
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/ResponseHelper.cs b/Emby.Server.Implementations/Services/ResponseHelper.cs
deleted file mode 100644
index a329b531d..000000000
--- a/Emby.Server.Implementations/Services/ResponseHelper.cs
+++ /dev/null
@@ -1,141 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Globalization;
-using System.IO;
-using System.Net;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-
-namespace Emby.Server.Implementations.Services
-{
- public static class ResponseHelper
- {
- public static Task WriteToResponse(HttpResponse response, IRequest request, object result, CancellationToken cancellationToken)
- {
- if (result == null)
- {
- if (response.StatusCode == (int)HttpStatusCode.OK)
- {
- response.StatusCode = (int)HttpStatusCode.NoContent;
- }
-
- response.ContentLength = 0;
- return Task.CompletedTask;
- }
-
- var httpResult = result as IHttpResult;
- if (httpResult != null)
- {
- httpResult.RequestContext = request;
- request.ResponseContentType = httpResult.ContentType ?? request.ResponseContentType;
- }
-
- var defaultContentType = request.ResponseContentType;
-
- if (httpResult != null)
- {
- if (httpResult.RequestContext == null)
- {
- httpResult.RequestContext = request;
- }
-
- response.StatusCode = httpResult.Status;
- }
-
- if (result is IHasHeaders responseOptions)
- {
- foreach (var responseHeaders in responseOptions.Headers)
- {
- if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))
- {
- response.ContentLength = long.Parse(responseHeaders.Value, CultureInfo.InvariantCulture);
- continue;
- }
-
- response.Headers.Add(responseHeaders.Key, responseHeaders.Value);
- }
- }
-
- // ContentType='text/html' is the default for a HttpResponse
- // Do not override if another has been set
- if (response.ContentType == null || response.ContentType == "text/html")
- {
- response.ContentType = defaultContentType;
- }
-
- if (response.ContentType == "application/json")
- {
- response.ContentType += "; charset=utf-8";
- }
-
- switch (result)
- {
- case IAsyncStreamWriter asyncStreamWriter:
- return asyncStreamWriter.WriteToAsync(response.Body, cancellationToken);
- case IStreamWriter streamWriter:
- streamWriter.WriteTo(response.Body);
- return Task.CompletedTask;
- case FileWriter fileWriter:
- return fileWriter.WriteToAsync(response, cancellationToken);
- case Stream stream:
- return CopyStream(stream, response.Body);
- case byte[] bytes:
- response.ContentType = "application/octet-stream";
- response.ContentLength = bytes.Length;
-
- if (bytes.Length > 0)
- {
- return response.Body.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
- }
-
- return Task.CompletedTask;
- case string responseText:
- var responseTextAsBytes = Encoding.UTF8.GetBytes(responseText);
- response.ContentLength = responseTextAsBytes.Length;
-
- if (responseTextAsBytes.Length > 0)
- {
- return response.Body.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken);
- }
-
- return Task.CompletedTask;
- }
-
- return WriteObject(request, result, response);
- }
-
- private static async Task CopyStream(Stream src, Stream dest)
- {
- using (src)
- {
- await src.CopyToAsync(dest).ConfigureAwait(false);
- }
- }
-
- public static async Task WriteObject(IRequest request, object result, HttpResponse response)
- {
- var contentType = request.ResponseContentType;
- var serializer = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
-
- using (var ms = new MemoryStream())
- {
- serializer(result, ms);
-
- ms.Position = 0;
-
- var contentLength = ms.Length;
- response.ContentLength = contentLength;
-
- if (contentLength > 0)
- {
- await ms.CopyToAsync(response.Body).ConfigureAwait(false);
- }
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs
deleted file mode 100644
index 47e7261e8..000000000
--- a/Emby.Server.Implementations/Services/ServiceController.cs
+++ /dev/null
@@ -1,202 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Services
-{
- public delegate object ActionInvokerFn(object intance, object request);
-
- public delegate void VoidActionInvokerFn(object intance, object request);
-
- public class ServiceController
- {
- private readonly ILogger _logger;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger.
- public ServiceController(ILogger logger)
- {
- _logger = logger;
- }
-
- public void Init(HttpListenerHost appHost, IEnumerable serviceTypes)
- {
- foreach (var serviceType in serviceTypes)
- {
- RegisterService(appHost, serviceType);
- }
- }
-
- public void RegisterService(HttpListenerHost appHost, Type serviceType)
- {
- // Make sure the provided type implements IService
- if (!typeof(IService).IsAssignableFrom(serviceType))
- {
- _logger.LogWarning("Tried to register a service that does not implement IService: {ServiceType}", serviceType);
- return;
- }
-
- var processedReqs = new HashSet();
-
- var actions = ServiceExecGeneral.Reset(serviceType);
-
- foreach (var mi in serviceType.GetActions())
- {
- var requestType = mi.GetParameters()[0].ParameterType;
- if (processedReqs.Contains(requestType))
- {
- continue;
- }
-
- processedReqs.Add(requestType);
-
- ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions);
-
- // var returnMarker = GetTypeWithGenericTypeDefinitionOf(requestType, typeof(IReturn<>));
- // var responseType = returnMarker != null ?
- // GetGenericArguments(returnMarker)[0]
- // : mi.ReturnType != typeof(object) && mi.ReturnType != typeof(void) ?
- // mi.ReturnType
- // : Type.GetType(requestType.FullName + "Response");
-
- RegisterRestPaths(appHost, requestType, serviceType);
-
- appHost.AddServiceInfo(serviceType, requestType);
- }
- }
-
- public readonly RestPath.RestPathMap RestPathMap = new RestPath.RestPathMap();
-
- public void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType)
- {
- var attrs = appHost.GetRouteAttributes(requestType);
- foreach (var attr in attrs)
- {
- var restPath = new RestPath(appHost.CreateInstance, appHost.GetParseFn, requestType, serviceType, attr.Path, attr.Verbs, attr.IsHidden, attr.Summary, attr.Description);
-
- RegisterRestPath(restPath);
- }
- }
-
- private static readonly char[] InvalidRouteChars = new[] { '?', '&' };
-
- public void RegisterRestPath(RestPath restPath)
- {
- if (restPath.Path[0] != '/')
- {
- throw new ArgumentException(
- string.Format(
- CultureInfo.InvariantCulture,
- "Route '{0}' on '{1}' must start with a '/'",
- restPath.Path,
- restPath.RequestType.GetMethodName()));
- }
-
- if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
- {
- throw new ArgumentException(
- string.Format(
- CultureInfo.InvariantCulture,
- "Route '{0}' on '{1}' contains invalid chars. ",
- restPath.Path,
- restPath.RequestType.GetMethodName()));
- }
-
- if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List pathsAtFirstMatch))
- {
- pathsAtFirstMatch.Add(restPath);
- }
- else
- {
- RestPathMap[restPath.FirstMatchHashKey] = new List() { restPath };
- }
- }
-
- public RestPath GetRestPathForRequest(string httpMethod, string pathInfo)
- {
- var matchUsingPathParts = RestPath.GetPathPartsForMatching(pathInfo);
-
- List firstMatches;
-
- var yieldedHashMatches = RestPath.GetFirstMatchHashKeys(matchUsingPathParts);
- foreach (var potentialHashMatch in yieldedHashMatches)
- {
- if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches))
- {
- continue;
- }
-
- var bestScore = -1;
- RestPath bestMatch = null;
- foreach (var restPath in firstMatches)
- {
- var score = restPath.MatchScore(httpMethod, matchUsingPathParts);
- if (score > bestScore)
- {
- bestScore = score;
- bestMatch = restPath;
- }
- }
-
- if (bestScore > 0 && bestMatch != null)
- {
- return bestMatch;
- }
- }
-
- var yieldedWildcardMatches = RestPath.GetFirstMatchWildCardHashKeys(matchUsingPathParts);
- foreach (var potentialHashMatch in yieldedWildcardMatches)
- {
- if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches))
- {
- continue;
- }
-
- var bestScore = -1;
- RestPath bestMatch = null;
- foreach (var restPath in firstMatches)
- {
- var score = restPath.MatchScore(httpMethod, matchUsingPathParts);
- if (score > bestScore)
- {
- bestScore = score;
- bestMatch = restPath;
- }
- }
-
- if (bestScore > 0 && bestMatch != null)
- {
- return bestMatch;
- }
- }
-
- return null;
- }
-
- public Task Execute(HttpListenerHost httpHost, object requestDto, IRequest req)
- {
- var requestType = requestDto.GetType();
- req.OperationName = requestType.Name;
-
- var serviceType = httpHost.GetServiceTypeByRequest(requestType);
-
- var service = httpHost.CreateInstance(serviceType);
-
- if (service is IRequiresRequest serviceRequiresContext)
- {
- serviceRequiresContext.Request = req;
- }
-
- // Executes the service and returns the result
- return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName());
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/ServiceExec.cs b/Emby.Server.Implementations/Services/ServiceExec.cs
deleted file mode 100644
index 7b970627e..000000000
--- a/Emby.Server.Implementations/Services/ServiceExec.cs
+++ /dev/null
@@ -1,230 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Linq.Expressions;
-using System.Reflection;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Server.Implementations.Services
-{
- public static class ServiceExecExtensions
- {
- public static string[] AllVerbs = new[] {
- "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", // RFC 2616
- "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", // RFC 2518
- "VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", "UNCHECKOUT",
- "MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY", // RFC 3253
- "ORDERPATCH", // RFC 3648
- "ACL", // RFC 3744
- "PATCH", // https://datatracker.ietf.org/doc/draft-dusseault-http-patch/
- "SEARCH", // https://datatracker.ietf.org/doc/draft-reschke-webdav-search/
- "BCOPY", "BDELETE", "BMOVE", "BPROPFIND", "BPROPPATCH", "NOTIFY",
- "POLL", "SUBSCRIBE", "UNSUBSCRIBE"
- };
-
- public static List GetActions(this Type serviceType)
- {
- var list = new List();
-
- foreach (var mi in serviceType.GetRuntimeMethods())
- {
- if (!mi.IsPublic)
- {
- continue;
- }
-
- if (mi.IsStatic)
- {
- continue;
- }
-
- if (mi.GetParameters().Length != 1)
- {
- continue;
- }
-
- var actionName = mi.Name;
- if (!AllVerbs.Contains(actionName, StringComparer.OrdinalIgnoreCase))
- {
- continue;
- }
-
- list.Add(mi);
- }
-
- return list;
- }
- }
-
- internal static class ServiceExecGeneral
- {
- private static Dictionary execMap = new Dictionary();
-
- public static void CreateServiceRunnersFor(Type requestType, List actions)
- {
- foreach (var actionCtx in actions)
- {
- if (execMap.ContainsKey(actionCtx.Id))
- {
- continue;
- }
-
- execMap[actionCtx.Id] = actionCtx;
- }
- }
-
- public static Task Execute(Type serviceType, IRequest request, object instance, object requestDto, string requestName)
- {
- var actionName = request.Verb ?? "POST";
-
- if (execMap.TryGetValue(ServiceMethod.Key(serviceType, actionName, requestName), out ServiceMethod actionContext))
- {
- if (actionContext.RequestFilters != null)
- {
- foreach (var requestFilter in actionContext.RequestFilters)
- {
- requestFilter.RequestFilter(request, request.Response, requestDto);
- if (request.Response.HasStarted)
- {
- Task.FromResult(null);
- }
- }
- }
-
- var response = actionContext.ServiceAction(instance, requestDto);
-
- if (response is Task taskResponse)
- {
- return GetTaskResult(taskResponse);
- }
-
- return Task.FromResult(response);
- }
-
- var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant();
- throw new NotImplementedException(
- string.Format(
- CultureInfo.InvariantCulture,
- "Could not find method named {1}({0}) or Any({0}) on Service {2}",
- requestDto.GetType().GetMethodName(),
- expectedMethodName,
- serviceType.GetMethodName()));
- }
-
- private static async Task GetTaskResult(Task task)
- {
- try
- {
- if (task is Task taskObject)
- {
- return await taskObject.ConfigureAwait(false);
- }
-
- await task.ConfigureAwait(false);
-
- var type = task.GetType().GetTypeInfo();
- if (!type.IsGenericType)
- {
- return null;
- }
-
- var resultProperty = type.GetDeclaredProperty("Result");
- if (resultProperty == null)
- {
- return null;
- }
-
- var result = resultProperty.GetValue(task);
-
- // hack alert
- if (result.GetType().Name.IndexOf("voidtaskresult", StringComparison.OrdinalIgnoreCase) != -1)
- {
- return null;
- }
-
- return result;
- }
- catch (TypeAccessException)
- {
- return null; // return null for void Task's
- }
- }
-
- public static List Reset(Type serviceType)
- {
- var actions = new List();
-
- foreach (var mi in serviceType.GetActions())
- {
- var actionName = mi.Name;
- var args = mi.GetParameters();
-
- var requestType = args[0].ParameterType;
- var actionCtx = new ServiceMethod
- {
- Id = ServiceMethod.Key(serviceType, actionName, requestType.GetMethodName())
- };
-
- actionCtx.ServiceAction = CreateExecFn(serviceType, requestType, mi);
-
- var reqFilters = new List();
-
- foreach (var attr in mi.GetCustomAttributes(true))
- {
- if (attr is IHasRequestFilter hasReqFilter)
- {
- reqFilters.Add(hasReqFilter);
- }
- }
-
- if (reqFilters.Count > 0)
- {
- actionCtx.RequestFilters = reqFilters.OrderBy(i => i.Priority).ToArray();
- }
-
- actions.Add(actionCtx);
- }
-
- return actions;
- }
-
- private static ActionInvokerFn CreateExecFn(Type serviceType, Type requestType, MethodInfo mi)
- {
- var serviceParam = Expression.Parameter(typeof(object), "serviceObj");
- var serviceStrong = Expression.Convert(serviceParam, serviceType);
-
- var requestDtoParam = Expression.Parameter(typeof(object), "requestDto");
- var requestDtoStrong = Expression.Convert(requestDtoParam, requestType);
-
- Expression callExecute = Expression.Call(
- serviceStrong, mi, requestDtoStrong);
-
- if (mi.ReturnType != typeof(void))
- {
- var executeFunc = Expression.Lambda(
- callExecute,
- serviceParam,
- requestDtoParam).Compile();
-
- return executeFunc;
- }
- else
- {
- var executeFunc = Expression.Lambda(
- callExecute,
- serviceParam,
- requestDtoParam).Compile();
-
- return (service, request) =>
- {
- executeFunc(service, request);
- return null;
- };
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs
deleted file mode 100644
index b4166f771..000000000
--- a/Emby.Server.Implementations/Services/ServiceHandler.cs
+++ /dev/null
@@ -1,212 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Net.Mime;
-using System.Reflection;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Services
-{
- public class ServiceHandler
- {
- private RestPath _restPath;
-
- private string _responseContentType;
-
- internal ServiceHandler(RestPath restPath, string responseContentType)
- {
- _restPath = restPath;
- _responseContentType = responseContentType;
- }
-
- protected static Task CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType)
- {
- if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0)
- {
- var deserializer = RequestHelper.GetRequestReader(host, contentType);
- if (deserializer != null)
- {
- return deserializer.Invoke(requestType, httpReq.InputStream);
- }
- }
-
- return Task.FromResult(host.CreateInstance(requestType));
- }
-
- public static string GetSanitizedPathInfo(string pathInfo, out string contentType)
- {
- contentType = null;
- var pos = pathInfo.LastIndexOf('.');
- if (pos != -1)
- {
- var format = pathInfo.AsSpan().Slice(pos + 1);
- contentType = GetFormatContentType(format);
- if (contentType != null)
- {
- pathInfo = pathInfo.Substring(0, pos);
- }
- }
-
- return pathInfo;
- }
-
- private static string GetFormatContentType(ReadOnlySpan format)
- {
- if (format.Equals("json", StringComparison.Ordinal))
- {
- return MediaTypeNames.Application.Json;
- }
- else if (format.Equals("xml", StringComparison.Ordinal))
- {
- return MediaTypeNames.Application.Xml;
- }
-
- return null;
- }
-
- public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, CancellationToken cancellationToken)
- {
- httpReq.Items["__route"] = _restPath;
-
- if (_responseContentType != null)
- {
- httpReq.ResponseContentType = _responseContentType;
- }
-
- var request = await CreateRequest(httpHost, httpReq, _restPath).ConfigureAwait(false);
-
- httpHost.ApplyRequestFilters(httpReq, httpRes, request);
-
- httpRes.HttpContext.SetServiceStackRequest(httpReq);
- var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
-
- // Apply response filters
- foreach (var responseFilter in httpHost.ResponseFilters)
- {
- responseFilter(httpReq, httpRes, response);
- }
-
- await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false);
- }
-
- public static async Task CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath)
- {
- var requestType = restPath.RequestType;
-
- if (RequireqRequestStream(requestType))
- {
- // Used by IRequiresRequestStream
- var requestParams = GetRequestParams(httpReq.Response.HttpContext.Request);
- var request = ServiceHandler.CreateRequest(httpReq, restPath, requestParams, host.CreateInstance(requestType));
-
- var rawReq = (IRequiresRequestStream)request;
- rawReq.RequestStream = httpReq.InputStream;
- return rawReq;
- }
- else
- {
- var requestParams = GetFlattenedRequestParams(httpReq.Response.HttpContext.Request);
-
- var requestDto = await CreateContentTypeRequest(host, httpReq, restPath.RequestType, httpReq.ContentType).ConfigureAwait(false);
-
- return CreateRequest(httpReq, restPath, requestParams, requestDto);
- }
- }
-
- public static bool RequireqRequestStream(Type requestType)
- {
- var requiresRequestStreamTypeInfo = typeof(IRequiresRequestStream).GetTypeInfo();
-
- return requiresRequestStreamTypeInfo.IsAssignableFrom(requestType.GetTypeInfo());
- }
-
- public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary requestParams, object requestDto)
- {
- var pathInfo = !restPath.IsWildCardPath
- ? GetSanitizedPathInfo(httpReq.PathInfo, out _)
- : httpReq.PathInfo;
-
- return restPath.CreateRequest(pathInfo, requestParams, requestDto);
- }
-
- ///
- /// Duplicate Params are given a unique key by appending a #1 suffix
- ///
- private static Dictionary GetRequestParams(HttpRequest request)
- {
- var map = new Dictionary();
-
- foreach (var pair in request.Query)
- {
- var values = pair.Value;
- if (values.Count == 1)
- {
- map[pair.Key] = values[0];
- }
- else
- {
- for (var i = 0; i < values.Count; i++)
- {
- map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i];
- }
- }
- }
-
- if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT"))
- && request.HasFormContentType)
- {
- foreach (var pair in request.Form)
- {
- var values = pair.Value;
- if (values.Count == 1)
- {
- map[pair.Key] = values[0];
- }
- else
- {
- for (var i = 0; i < values.Count; i++)
- {
- map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i];
- }
- }
- }
- }
-
- return map;
- }
-
- private static bool IsMethod(string method, string expected)
- => string.Equals(method, expected, StringComparison.OrdinalIgnoreCase);
-
- ///
- /// Duplicate params have their values joined together in a comma-delimited string.
- ///
- private static Dictionary GetFlattenedRequestParams(HttpRequest request)
- {
- var map = new Dictionary();
-
- foreach (var pair in request.Query)
- {
- map[pair.Key] = pair.Value;
- }
-
- if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT"))
- && request.HasFormContentType)
- {
- foreach (var pair in request.Form)
- {
- map[pair.Key] = pair.Value;
- }
- }
-
- return map;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/ServiceMethod.cs b/Emby.Server.Implementations/Services/ServiceMethod.cs
deleted file mode 100644
index 5116cc04f..000000000
--- a/Emby.Server.Implementations/Services/ServiceMethod.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-
-namespace Emby.Server.Implementations.Services
-{
- public class ServiceMethod
- {
- public string Id { get; set; }
-
- public ActionInvokerFn ServiceAction { get; set; }
-
- public MediaBrowser.Model.Services.IHasRequestFilter[] RequestFilters { get; set; }
-
- public static string Key(Type serviceType, string method, string requestDtoName)
- {
- return serviceType.FullName + " " + method.ToUpperInvariant() + " " + requestDtoName;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/ServicePath.cs b/Emby.Server.Implementations/Services/ServicePath.cs
deleted file mode 100644
index 0d4728b43..000000000
--- a/Emby.Server.Implementations/Services/ServicePath.cs
+++ /dev/null
@@ -1,550 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-using System.Text;
-using System.Text.Json.Serialization;
-
-namespace Emby.Server.Implementations.Services
-{
- public class RestPath
- {
- private const string WildCard = "*";
- private const char WildCardChar = '*';
- private const string PathSeperator = "/";
- private const char PathSeperatorChar = '/';
- private const char ComponentSeperator = '.';
- private const string VariablePrefix = "{";
-
- private readonly bool[] componentsWithSeparators;
-
- private readonly string restPath;
- public bool IsWildCardPath { get; private set; }
-
- private readonly string[] literalsToMatch;
-
- private readonly string[] variablesNames;
-
- private readonly bool[] isWildcard;
- private readonly int wildcardCount = 0;
-
- internal static string[] IgnoreAttributesNamed = new[]
- {
- nameof(JsonIgnoreAttribute)
- };
-
- private static Type _excludeType = typeof(Stream);
-
- public int VariableArgsCount { get; set; }
-
- ///
- /// The number of segments separated by '/' determinable by path.Split('/').Length
- /// e.g. /path/to/here.ext == 3
- ///
- public int PathComponentsCount { get; set; }
-
- ///
- /// Gets or sets the total number of segments after subparts have been exploded ('.')
- /// e.g. /path/to/here.ext == 4.
- ///
- public int TotalComponentsCount { get; set; }
-
- public string[] Verbs { get; private set; }
-
- public Type RequestType { get; private set; }
-
- public Type ServiceType { get; private set; }
-
- public string Path => this.restPath;
-
- public string Summary { get; private set; }
-
- public string Description { get; private set; }
-
- public bool IsHidden { get; private set; }
-
- public static string[] GetPathPartsForMatching(string pathInfo)
- {
- return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- public static List GetFirstMatchHashKeys(string[] pathPartsForMatching)
- {
- var hashPrefix = pathPartsForMatching.Length + PathSeperator;
- return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching);
- }
-
- public static List GetFirstMatchWildCardHashKeys(string[] pathPartsForMatching)
- {
- const string HashPrefix = WildCard + PathSeperator;
- return GetPotentialMatchesWithPrefix(HashPrefix, pathPartsForMatching);
- }
-
- private static List GetPotentialMatchesWithPrefix(string hashPrefix, string[] pathPartsForMatching)
- {
- var list = new List();
-
- foreach (var part in pathPartsForMatching)
- {
- list.Add(hashPrefix + part);
-
- if (part.IndexOf(ComponentSeperator, StringComparison.Ordinal) == -1)
- {
- continue;
- }
-
- var subParts = part.Split(ComponentSeperator);
- foreach (var subPart in subParts)
- {
- list.Add(hashPrefix + subPart);
- }
- }
-
- return list;
- }
-
- public RestPath(Func createInstanceFn, Func> getParseFn, Type requestType, Type serviceType, string path, string verbs, bool isHidden = false, string summary = null, string description = null)
- {
- this.RequestType = requestType;
- this.ServiceType = serviceType;
- this.Summary = summary;
- this.IsHidden = isHidden;
- this.Description = description;
- this.restPath = path;
-
- this.Verbs = string.IsNullOrWhiteSpace(verbs) ? ServiceExecExtensions.AllVerbs : verbs.ToUpperInvariant().Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries);
-
- var componentsList = new List();
-
- // We only split on '.' if the restPath has them. Allows for /{action}.{type}
- var hasSeparators = new List();
- foreach (var component in this.restPath.Split(PathSeperatorChar))
- {
- if (string.IsNullOrEmpty(component))
- {
- continue;
- }
-
- if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1
- && component.IndexOf(ComponentSeperator, StringComparison.Ordinal) != -1)
- {
- hasSeparators.Add(true);
- componentsList.AddRange(component.Split(ComponentSeperator));
- }
- else
- {
- hasSeparators.Add(false);
- componentsList.Add(component);
- }
- }
-
- var components = componentsList.ToArray();
- this.TotalComponentsCount = components.Length;
-
- this.literalsToMatch = new string[this.TotalComponentsCount];
- this.variablesNames = new string[this.TotalComponentsCount];
- this.isWildcard = new bool[this.TotalComponentsCount];
- this.componentsWithSeparators = hasSeparators.ToArray();
- this.PathComponentsCount = this.componentsWithSeparators.Length;
- string firstLiteralMatch = null;
-
- for (var i = 0; i < components.Length; i++)
- {
- var component = components[i];
-
- if (component.StartsWith(VariablePrefix, StringComparison.Ordinal))
- {
- var variableName = component.Substring(1, component.Length - 2);
- if (variableName[variableName.Length - 1] == WildCardChar)
- {
- this.isWildcard[i] = true;
- variableName = variableName.Substring(0, variableName.Length - 1);
- }
-
- this.variablesNames[i] = variableName;
- this.VariableArgsCount++;
- }
- else
- {
- this.literalsToMatch[i] = component.ToLowerInvariant();
-
- if (firstLiteralMatch == null)
- {
- firstLiteralMatch = this.literalsToMatch[i];
- }
- }
- }
-
- for (var i = 0; i < components.Length - 1; i++)
- {
- if (!this.isWildcard[i])
- {
- continue;
- }
-
- if (this.literalsToMatch[i + 1] == null)
- {
- throw new ArgumentException(
- "A wildcard path component must be at the end of the path or followed by a literal path component.");
- }
- }
-
- this.wildcardCount = this.isWildcard.Length;
- this.IsWildCardPath = this.wildcardCount > 0;
-
- this.FirstMatchHashKey = !this.IsWildCardPath
- ? this.PathComponentsCount + PathSeperator + firstLiteralMatch
- : WildCardChar + PathSeperator + firstLiteralMatch;
-
- this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType);
-
- _propertyNamesMap = new HashSet(
- GetSerializableProperties(RequestType).Select(x => x.Name),
- StringComparer.OrdinalIgnoreCase);
- }
-
- internal static IEnumerable GetSerializableProperties(Type type)
- {
- foreach (var prop in GetPublicProperties(type))
- {
- if (prop.GetMethod == null
- || _excludeType == prop.PropertyType)
- {
- continue;
- }
-
- var ignored = false;
- foreach (var attr in prop.GetCustomAttributes(true))
- {
- if (IgnoreAttributesNamed.Contains(attr.GetType().Name))
- {
- ignored = true;
- break;
- }
- }
-
- if (!ignored)
- {
- yield return prop;
- }
- }
- }
-
- private static IEnumerable GetPublicProperties(Type type)
- {
- if (type.IsInterface)
- {
- var propertyInfos = new List();
- var considered = new List()
- {
- type
- };
- var queue = new Queue();
- queue.Enqueue(type);
-
- while (queue.Count > 0)
- {
- var subType = queue.Dequeue();
- foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces)
- {
- if (considered.Contains(subInterface))
- {
- continue;
- }
-
- considered.Add(subInterface);
- queue.Enqueue(subInterface);
- }
-
- var newPropertyInfos = GetTypesPublicProperties(subType)
- .Where(x => !propertyInfos.Contains(x));
-
- propertyInfos.InsertRange(0, newPropertyInfos);
- }
-
- return propertyInfos;
- }
-
- return GetTypesPublicProperties(type)
- .Where(x => x.GetIndexParameters().Length == 0);
- }
-
- private static IEnumerable GetTypesPublicProperties(Type subType)
- {
- foreach (var pi in subType.GetRuntimeProperties())
- {
- var mi = pi.GetMethod ?? pi.SetMethod;
- if (mi != null && mi.IsStatic)
- {
- continue;
- }
-
- yield return pi;
- }
- }
-
- ///
- /// Provide for quick lookups based on hashes that can be determined from a request url.
- ///
- public string FirstMatchHashKey { get; private set; }
-
- private readonly StringMapTypeDeserializer typeDeserializer;
-
- private readonly HashSet _propertyNamesMap;
-
- public int MatchScore(string httpMethod, string[] withPathInfoParts)
- {
- var isMatch = IsMatch(httpMethod, withPathInfoParts, out var wildcardMatchCount);
- if (!isMatch)
- {
- return -1;
- }
-
- // Routes with least wildcard matches get the highest score
- var score = Math.Max(100 - wildcardMatchCount, 1) * 1000
- // Routes with less variable (and more literal) matches
- + Math.Max(10 - VariableArgsCount, 1) * 100;
-
- // Exact verb match is better than ANY
- if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase))
- {
- score += 10;
- }
- else
- {
- score += 1;
- }
-
- return score;
- }
-
- ///
- /// For performance withPathInfoParts should already be a lower case string
- /// to minimize redundant matching operations.
- ///
- public bool IsMatch(string httpMethod, string[] withPathInfoParts, out int wildcardMatchCount)
- {
- wildcardMatchCount = 0;
-
- if (withPathInfoParts.Length != this.PathComponentsCount && !this.IsWildCardPath)
- {
- return false;
- }
-
- if (!Verbs.Contains(httpMethod, StringComparer.OrdinalIgnoreCase))
- {
- return false;
- }
-
- if (!ExplodeComponents(ref withPathInfoParts))
- {
- return false;
- }
-
- if (this.TotalComponentsCount != withPathInfoParts.Length && !this.IsWildCardPath)
- {
- return false;
- }
-
- int pathIx = 0;
- for (var i = 0; i < this.TotalComponentsCount; i++)
- {
- if (this.isWildcard[i])
- {
- if (i < this.TotalComponentsCount - 1)
- {
- // Continue to consume up until a match with the next literal
- while (pathIx < withPathInfoParts.Length
- && !string.Equals(withPathInfoParts[pathIx], this.literalsToMatch[i + 1], StringComparison.InvariantCultureIgnoreCase))
- {
- pathIx++;
- wildcardMatchCount++;
- }
-
- // Ensure there are still enough parts left to match the remainder
- if ((withPathInfoParts.Length - pathIx) < (this.TotalComponentsCount - i - 1))
- {
- return false;
- }
- }
- else
- {
- // A wildcard at the end matches the remainder of path
- wildcardMatchCount += withPathInfoParts.Length - pathIx;
- pathIx = withPathInfoParts.Length;
- }
- }
- else
- {
- var literalToMatch = this.literalsToMatch[i];
- if (literalToMatch == null)
- {
- // Matching an ordinary (non-wildcard) variable consumes a single part
- pathIx++;
- continue;
- }
-
- if (withPathInfoParts.Length <= pathIx
- || !string.Equals(withPathInfoParts[pathIx], literalToMatch, StringComparison.InvariantCultureIgnoreCase))
- {
- return false;
- }
-
- pathIx++;
- }
- }
-
- return pathIx == withPathInfoParts.Length;
- }
-
- private bool ExplodeComponents(ref string[] withPathInfoParts)
- {
- var totalComponents = new List();
- for (var i = 0; i < withPathInfoParts.Length; i++)
- {
- var component = withPathInfoParts[i];
- if (string.IsNullOrEmpty(component))
- {
- continue;
- }
-
- if (this.PathComponentsCount != this.TotalComponentsCount
- && this.componentsWithSeparators[i])
- {
- var subComponents = component.Split(ComponentSeperator);
- if (subComponents.Length < 2)
- {
- return false;
- }
-
- totalComponents.AddRange(subComponents);
- }
- else
- {
- totalComponents.Add(component);
- }
- }
-
- withPathInfoParts = totalComponents.ToArray();
- return true;
- }
-
- public object CreateRequest(string pathInfo, Dictionary queryStringAndFormData, object fromInstance)
- {
- var requestComponents = pathInfo.Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
-
- ExplodeComponents(ref requestComponents);
-
- if (requestComponents.Length != this.TotalComponentsCount)
- {
- var isValidWildCardPath = this.IsWildCardPath
- && requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount;
-
- if (!isValidWildCardPath)
- {
- throw new ArgumentException(
- string.Format(
- CultureInfo.InvariantCulture,
- "Path Mismatch: Request Path '{0}' has invalid number of components compared to: '{1}'",
- pathInfo,
- this.restPath));
- }
- }
-
- var requestKeyValuesMap = new Dictionary();
- var pathIx = 0;
- for (var i = 0; i < this.TotalComponentsCount; i++)
- {
- var variableName = this.variablesNames[i];
- if (variableName == null)
- {
- pathIx++;
- continue;
- }
-
- if (!this._propertyNamesMap.Contains(variableName))
- {
- if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase))
- {
- pathIx++;
- continue;
- }
-
- throw new ArgumentException("Could not find property "
- + variableName + " on " + RequestType.GetMethodName());
- }
-
- var value = requestComponents.Length > pathIx ? requestComponents[pathIx] : null; // wildcard has arg mismatch
- if (value != null && this.isWildcard[i])
- {
- if (i == this.TotalComponentsCount - 1)
- {
- // Wildcard at end of path definition consumes all the rest
- var sb = new StringBuilder();
- sb.Append(value);
- for (var j = pathIx + 1; j < requestComponents.Length; j++)
- {
- sb.Append(PathSeperatorChar)
- .Append(requestComponents[j]);
- }
-
- value = sb.ToString();
- }
- else
- {
- // Wildcard in middle of path definition consumes up until it
- // hits a match for the next element in the definition (which must be a literal)
- // It may consume 0 or more path parts
- var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1];
- if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
- {
- var sb = new StringBuilder(value);
- pathIx++;
- while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
- {
- sb.Append(PathSeperatorChar)
- .Append(requestComponents[pathIx++]);
- }
-
- value = sb.ToString();
- }
- else
- {
- value = null;
- }
- }
- }
- else
- {
- // Variable consumes single path item
- pathIx++;
- }
-
- requestKeyValuesMap[variableName] = value;
- }
-
- if (queryStringAndFormData != null)
- {
- // Query String and form data can override variable path matches
- // path variables < query string < form data
- foreach (var name in queryStringAndFormData)
- {
- requestKeyValuesMap[name.Key] = name.Value;
- }
- }
-
- return this.typeDeserializer.PopulateFromMap(fromInstance, requestKeyValuesMap);
- }
-
- public class RestPathMap : SortedDictionary>
- {
- public RestPathMap() : base(StringComparer.OrdinalIgnoreCase)
- {
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
deleted file mode 100644
index 165bb0fc4..000000000
--- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
+++ /dev/null
@@ -1,118 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Reflection;
-using MediaBrowser.Common.Extensions;
-
-namespace Emby.Server.Implementations.Services
-{
- ///
- /// Serializer cache of delegates required to create a type from a string map (e.g. for REST urls)
- ///
- public class StringMapTypeDeserializer
- {
- internal class PropertySerializerEntry
- {
- public PropertySerializerEntry(Action propertySetFn, Func propertyParseStringFn, Type propertyType)
- {
- PropertySetFn = propertySetFn;
- PropertyParseStringFn = propertyParseStringFn;
- PropertyType = propertyType;
- }
-
- public Action PropertySetFn { get; private set; }
-
- public Func PropertyParseStringFn { get; private set; }
-
- public Type PropertyType { get; private set; }
- }
-
- private readonly Type type;
- private readonly Dictionary propertySetterMap
- = new Dictionary(StringComparer.OrdinalIgnoreCase);
-
- public Func GetParseFn(Type propertyType)
- {
- if (propertyType == typeof(string))
- {
- return s => s;
- }
-
- return _GetParseFn(propertyType);
- }
-
- private readonly Func _CreateInstanceFn;
- private readonly Func> _GetParseFn;
-
- public StringMapTypeDeserializer(Func createInstanceFn, Func> getParseFn, Type type)
- {
- _CreateInstanceFn = createInstanceFn;
- _GetParseFn = getParseFn;
- this.type = type;
-
- foreach (var propertyInfo in RestPath.GetSerializableProperties(type))
- {
- var propertySetFn = TypeAccessor.GetSetPropertyMethod(propertyInfo);
- var propertyType = propertyInfo.PropertyType;
- var propertyParseStringFn = GetParseFn(propertyType);
- var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType);
-
- propertySetterMap[propertyInfo.Name] = propertySerializer;
- }
- }
-
- public object PopulateFromMap(object instance, IDictionary keyValuePairs)
- {
- PropertySerializerEntry propertySerializerEntry = null;
-
- if (instance == null)
- {
- instance = _CreateInstanceFn(type);
- }
-
- foreach (var pair in keyValuePairs)
- {
- string propertyName = pair.Key;
- string propertyTextValue = pair.Value;
-
- if (propertyTextValue == null
- || !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)
- || propertySerializerEntry.PropertySetFn == null)
- {
- continue;
- }
-
- if (propertySerializerEntry.PropertyType == typeof(bool))
- {
- // InputExtensions.cs#530 MVC Checkbox helper emits extra hidden input field, generating 2 values, first is the real value
- propertyTextValue = StringExtensions.LeftPart(propertyTextValue, ',').ToString();
- }
-
- var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue);
- if (value == null)
- {
- continue;
- }
-
- propertySerializerEntry.PropertySetFn(instance, value);
- }
-
- return instance;
- }
- }
-
- internal static class TypeAccessor
- {
- public static Action GetSetPropertyMethod(PropertyInfo propertyInfo)
- {
- if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0)
- {
- return null;
- }
-
- var setMethodInfo = propertyInfo.SetMethod;
- return (instance, value) => setMethodInfo.Invoke(instance, new[] { value });
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/UrlExtensions.cs b/Emby.Server.Implementations/Services/UrlExtensions.cs
deleted file mode 100644
index 92e36b60e..000000000
--- a/Emby.Server.Implementations/Services/UrlExtensions.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using MediaBrowser.Common.Extensions;
-
-namespace Emby.Server.Implementations.Services
-{
- ///
- /// Donated by Ivan Korneliuk from his post:
- /// http://korneliuk.blogspot.com/2012/08/servicestack-reusing-dtos.html
- ///
- /// Modified to only allow using routes matching the supplied HTTP Verb.
- ///
- public static class UrlExtensions
- {
- public static string GetMethodName(this Type type)
- {
- var typeName = type.FullName != null // can be null, e.g. generic types
- ? StringExtensions.LeftPart(type.FullName, "[[", StringComparison.Ordinal).ToString() // Generic Fullname
- .Replace(type.Namespace + ".", string.Empty, StringComparison.Ordinal) // Trim Namespaces
- .Replace("+", ".", StringComparison.Ordinal) // Convert nested into normal type
- : type.Name;
-
- return type.IsGenericParameter ? "'" + typeName : typeName;
- }
- }
-}
diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
deleted file mode 100644
index ae1a8d0b7..000000000
--- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
+++ /dev/null
@@ -1,248 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Net.Mime;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Primitives;
-using Microsoft.Net.Http.Headers;
-using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest;
-
-namespace Emby.Server.Implementations.SocketSharp
-{
- public class WebSocketSharpRequest : IHttpRequest
- {
- private const string FormUrlEncoded = "application/x-www-form-urlencoded";
- private const string MultiPartFormData = "multipart/form-data";
- private const string Soap11 = "text/xml; charset=utf-8";
-
- private string _remoteIp;
- private Dictionary _items;
- private string _responseContentType;
-
- public WebSocketSharpRequest(HttpRequest httpRequest, HttpResponse httpResponse, string operationName)
- {
- this.OperationName = operationName;
- this.Request = httpRequest;
- this.Response = httpResponse;
- }
-
- public string Accept => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Accept]) ? null : Request.Headers[HeaderNames.Accept].ToString();
-
- public string Authorization => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Authorization]) ? null : Request.Headers[HeaderNames.Authorization].ToString();
-
- public HttpRequest Request { get; }
-
- public HttpResponse Response { get; }
-
- public string OperationName { get; set; }
-
- public string RawUrl => Request.GetEncodedPathAndQuery();
-
- public string AbsoluteUri => Request.GetDisplayUrl().TrimEnd('/');
-
- public string RemoteIp
- {
- get
- {
- if (_remoteIp != null)
- {
- return _remoteIp;
- }
-
- IPAddress ip;
-
- // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip
- // (if the server is behind a reverse proxy for example)
- if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XForwardedFor), out ip))
- {
- if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip))
- {
- ip = Request.HttpContext.Connection.RemoteIpAddress;
-
- // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests)
- ip ??= IPAddress.Loopback;
- }
- }
-
- return _remoteIp = NormalizeIp(ip).ToString();
- }
- }
-
- public string[] AcceptTypes => Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
-
- public Dictionary Items => _items ?? (_items = new Dictionary());
-
- public string ResponseContentType
- {
- get =>
- _responseContentType
- ?? (_responseContentType = GetResponseContentType(Request));
- set => _responseContentType = value;
- }
-
- public string PathInfo => Request.Path.Value;
-
- public string UserAgent => Request.Headers[HeaderNames.UserAgent];
-
- public IHeaderDictionary Headers => Request.Headers;
-
- public IQueryCollection QueryString => Request.Query;
-
- public bool IsLocal =>
- (Request.HttpContext.Connection.LocalIpAddress == null
- && Request.HttpContext.Connection.RemoteIpAddress == null)
- || Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress);
-
- public string HttpMethod => Request.Method;
-
- public string Verb => HttpMethod;
-
- public string ContentType => Request.ContentType;
-
- public Uri UrlReferrer => Request.GetTypedHeaders().Referer;
-
- public Stream InputStream => Request.Body;
-
- public long ContentLength => Request.ContentLength ?? 0;
-
- private string GetHeader(string name) => Request.Headers[name].ToString();
-
- private static IPAddress NormalizeIp(IPAddress ip)
- {
- if (ip.IsIPv4MappedToIPv6)
- {
- return ip.MapToIPv4();
- }
-
- return ip;
- }
-
- public static string GetResponseContentType(HttpRequest httpReq)
- {
- var specifiedContentType = GetQueryStringContentType(httpReq);
- if (!string.IsNullOrEmpty(specifiedContentType))
- {
- return specifiedContentType;
- }
-
- const string ServerDefaultContentType = MediaTypeNames.Application.Json;
-
- var acceptContentTypes = httpReq.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
- string defaultContentType = null;
- if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData))
- {
- defaultContentType = ServerDefaultContentType;
- }
-
- var acceptsAnything = false;
- var hasDefaultContentType = defaultContentType != null;
- if (acceptContentTypes != null)
- {
- foreach (ReadOnlySpan acceptsType in acceptContentTypes)
- {
- ReadOnlySpan contentType = acceptsType;
- var index = contentType.IndexOf(';');
- if (index != -1)
- {
- contentType = contentType.Slice(0, index);
- }
-
- contentType = contentType.Trim();
- acceptsAnything = contentType.Equals("*/*", StringComparison.OrdinalIgnoreCase);
-
- if (acceptsAnything)
- {
- break;
- }
- }
-
- if (acceptsAnything)
- {
- if (hasDefaultContentType)
- {
- return defaultContentType;
- }
- else
- {
- return ServerDefaultContentType;
- }
- }
- }
-
- if (acceptContentTypes == null && httpReq.ContentType == Soap11)
- {
- return Soap11;
- }
-
- // We could also send a '406 Not Acceptable', but this is allowed also
- return ServerDefaultContentType;
- }
-
- public static bool HasAnyOfContentTypes(HttpRequest request, params string[] contentTypes)
- {
- if (contentTypes == null || request.ContentType == null)
- {
- return false;
- }
-
- foreach (var contentType in contentTypes)
- {
- if (IsContentType(request, contentType))
- {
- return true;
- }
- }
-
- return false;
- }
-
- public static bool IsContentType(HttpRequest request, string contentType)
- {
- return request.ContentType.StartsWith(contentType, StringComparison.OrdinalIgnoreCase);
- }
-
- private static string GetQueryStringContentType(HttpRequest httpReq)
- {
- ReadOnlySpan format = httpReq.Query["format"].ToString();
- if (format == ReadOnlySpan.Empty)
- {
- const int FormatMaxLength = 4;
- ReadOnlySpan pi = httpReq.Path.ToString();
- if (pi == null || pi.Length <= FormatMaxLength)
- {
- return null;
- }
-
- if (pi[0] == '/')
- {
- pi = pi.Slice(1);
- }
-
- format = pi.LeftPart('/');
- if (format.Length > FormatMaxLength)
- {
- return null;
- }
- }
-
- format = format.LeftPart('.');
- if (format.Contains("json", StringComparison.OrdinalIgnoreCase))
- {
- return "application/json";
- }
- else if (format.Contains("xml", StringComparison.OrdinalIgnoreCase))
- {
- return "application/xml";
- }
-
- return null;
- }
- }
-}
diff --git a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs
index d746207c7..76db68885 100644
--- a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs
+++ b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs
@@ -1,4 +1,5 @@
-using MediaBrowser.Model.Services;
+using System.Net;
+using MediaBrowser.Common.Net;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Common.Extensions
@@ -8,26 +9,54 @@ namespace MediaBrowser.Common.Extensions
///
public static class HttpContextExtensions
{
- private const string ServiceStackRequest = "ServiceStackRequest";
-
///
- /// Set the ServiceStack request.
+ /// Checks the origin of the HTTP request.
///
- /// The HttpContext instance.
- /// The service stack request instance.
- public static void SetServiceStackRequest(this HttpContext httpContext, IRequest request)
+ /// The incoming HTTP request.
+ /// true if the request is coming from LAN, false otherwise.
+ public static bool IsLocal(this HttpRequest request)
{
- httpContext.Items[ServiceStackRequest] = request;
+ return (request.HttpContext.Connection.LocalIpAddress == null
+ && request.HttpContext.Connection.RemoteIpAddress == null)
+ || request.HttpContext.Connection.LocalIpAddress.Equals(request.HttpContext.Connection.RemoteIpAddress);
}
///
- /// Get the ServiceStack request.
+ /// Extracts the remote IP address of the caller of the HTTP request.
///
- /// The HttpContext instance.
- /// The service stack request instance.
- public static IRequest GetServiceStackRequest(this HttpContext httpContext)
+ /// The HTTP request.
+ /// The remote caller IP address.
+ public static string RemoteIp(this HttpRequest request)
{
- return (IRequest)httpContext.Items[ServiceStackRequest];
+ if (string.IsNullOrEmpty(request.HttpContext.Items["RemoteIp"].ToString()))
+ {
+ return request.HttpContext.Items["RemoteIp"].ToString();
+ }
+
+ IPAddress ip;
+
+ // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip
+ // (if the server is behind a reverse proxy for example)
+ if (!IPAddress.TryParse(request.Headers[CustomHeaderNames.XForwardedFor].ToString(), out ip))
+ {
+ if (!IPAddress.TryParse(request.Headers[CustomHeaderNames.XRealIP].ToString(), out ip))
+ {
+ ip = request.HttpContext.Connection.RemoteIpAddress;
+
+ // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests)
+ ip ??= IPAddress.Loopback;
+ }
+ }
+
+ if (ip.IsIPv4MappedToIPv6)
+ {
+ ip = ip.MapToIPv4();
+ }
+
+ var normalizedIp = ip.ToString();
+
+ request.HttpContext.Items["RemoteIp"] = normalizedIp;
+ return normalizedIp;
}
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
index 4cbb63e46..1f3abe8f4 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
@@ -4,7 +4,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Services;
namespace MediaBrowser.Controller.MediaEncoding
{
@@ -63,26 +62,20 @@ namespace MediaBrowser.Controller.MediaEncoding
/// Gets or sets the id.
///
/// The id.
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public Guid Id { get; set; }
- [ApiMember(Name = "MediaSourceId", Description = "The media version id, if playing an alternate version", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string MediaSourceId { get; set; }
- [ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string DeviceId { get; set; }
- [ApiMember(Name = "Container", Description = "Container", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
public string Container { get; set; }
///
/// Gets or sets the audio codec.
///
/// The audio codec.
- [ApiMember(Name = "AudioCodec", Description = "Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string AudioCodec { get; set; }
- [ApiMember(Name = "EnableAutoStreamCopy", Description = "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool EnableAutoStreamCopy { get; set; }
public bool AllowVideoStreamCopy { get; set; }
@@ -95,7 +88,6 @@ namespace MediaBrowser.Controller.MediaEncoding
/// Gets or sets the audio sample rate.
///
/// The audio sample rate.
- [ApiMember(Name = "AudioSampleRate", Description = "Optional. Specify a specific audio sample rate, e.g. 44100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? AudioSampleRate { get; set; }
public int? MaxAudioBitDepth { get; set; }
@@ -104,105 +96,86 @@ namespace MediaBrowser.Controller.MediaEncoding
/// Gets or sets the audio bit rate.
///
/// The audio bit rate.
- [ApiMember(Name = "AudioBitRate", Description = "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? AudioBitRate { get; set; }
///
/// Gets or sets the audio channels.
///
/// The audio channels.
- [ApiMember(Name = "AudioChannels", Description = "Optional. Specify a specific number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? AudioChannels { get; set; }
- [ApiMember(Name = "MaxAudioChannels", Description = "Optional. Specify a maximum number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? MaxAudioChannels { get; set; }
- [ApiMember(Name = "Static", Description = "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool Static { get; set; }
///
/// Gets or sets the profile.
///
/// The profile.
- [ApiMember(Name = "Profile", Description = "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string Profile { get; set; }
///
/// Gets or sets the level.
///
/// The level.
- [ApiMember(Name = "Level", Description = "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string Level { get; set; }
///
/// Gets or sets the framerate.
///
/// The framerate.
- [ApiMember(Name = "Framerate", Description = "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")]
public float? Framerate { get; set; }
- [ApiMember(Name = "MaxFramerate", Description = "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")]
public float? MaxFramerate { get; set; }
- [ApiMember(Name = "CopyTimestamps", Description = "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool CopyTimestamps { get; set; }
///
/// Gets or sets the start time ticks.
///
/// The start time ticks.
- [ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public long? StartTimeTicks { get; set; }
///
/// Gets or sets the width.
///
/// The width.
- [ApiMember(Name = "Width", Description = "Optional. The fixed horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? Width { get; set; }
///
/// Gets or sets the height.
///
/// The height.
- [ApiMember(Name = "Height", Description = "Optional. The fixed vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? Height { get; set; }
///
/// Gets or sets the width of the max.
///
/// The width of the max.
- [ApiMember(Name = "MaxWidth", Description = "Optional. The maximum horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? MaxWidth { get; set; }
///
/// Gets or sets the height of the max.
///
/// The height of the max.
- [ApiMember(Name = "MaxHeight", Description = "Optional. The maximum vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? MaxHeight { get; set; }
///
/// Gets or sets the video bit rate.
///
/// The video bit rate.
- [ApiMember(Name = "VideoBitRate", Description = "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? VideoBitRate { get; set; }
///
/// Gets or sets the index of the subtitle stream.
///
/// The index of the subtitle stream.
- [ApiMember(Name = "SubtitleStreamIndex", Description = "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? SubtitleStreamIndex { get; set; }
- [ApiMember(Name = "SubtitleMethod", Description = "Optional. Specify the subtitle delivery method.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public SubtitleDeliveryMethod SubtitleMethod { get; set; }
- [ApiMember(Name = "MaxRefFrames", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? MaxRefFrames { get; set; }
- [ApiMember(Name = "MaxVideoBitDepth", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? MaxVideoBitDepth { get; set; }
public bool RequireAvc { get; set; }
@@ -223,7 +196,6 @@ namespace MediaBrowser.Controller.MediaEncoding
/// Gets or sets the video codec.
///
/// The video codec.
- [ApiMember(Name = "VideoCodec", Description = "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string VideoCodec { get; set; }
public string SubtitleCodec { get; set; }
@@ -234,14 +206,12 @@ namespace MediaBrowser.Controller.MediaEncoding
/// Gets or sets the index of the audio stream.
///
/// The index of the audio stream.
- [ApiMember(Name = "AudioStreamIndex", Description = "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? AudioStreamIndex { get; set; }
///
/// Gets or sets the index of the video stream.
///
/// The index of the video stream.
- [ApiMember(Name = "VideoStreamIndex", Description = "Optional. The index of the video stream to use. If omitted the first video stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? VideoStreamIndex { get; set; }
public EncodingContext Context { get; set; }
diff --git a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs
deleted file mode 100644
index 1366fd42e..000000000
--- a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-
-namespace MediaBrowser.Controller.Net
-{
- public class AuthenticatedAttribute : Attribute, IHasRequestFilter, IAuthenticationAttributes
- {
- public static IAuthService AuthService { get; set; }
-
- ///
- /// Gets or sets the roles.
- ///
- /// The roles.
- public string Roles { get; set; }
-
- ///
- /// Gets or sets a value indicating whether [escape parental control].
- ///
- /// true if [escape parental control]; otherwise, false.
- public bool EscapeParentalControl { get; set; }
-
- ///
- /// Gets or sets a value indicating whether [allow before startup wizard].
- ///
- /// true if [allow before startup wizard]; otherwise, false.
- public bool AllowBeforeStartupWizard { get; set; }
-
- public bool AllowLocal { get; set; }
-
- ///
- /// The request filter is executed before the service.
- ///
- /// The http request wrapper.
- /// The http response wrapper.
- /// The request DTO.
- public void RequestFilter(IRequest request, HttpResponse response, object requestDto)
- {
- AuthService.Authenticate(request, this);
- }
-
- ///
- /// Order in which Request Filters are executed.
- /// <0 Executed before global request filters
- /// >0 Executed after global request filters
- ///
- /// The priority.
- public int Priority => 0;
-
- public string[] GetRoles()
- {
- return (Roles ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- public bool IgnoreLegacyAuth { get; set; }
-
- public bool AllowLocalOnly { get; set; }
- }
-
- public interface IAuthenticationAttributes
- {
- bool EscapeParentalControl { get; }
-
- bool AllowBeforeStartupWizard { get; }
-
- bool AllowLocal { get; }
-
- bool AllowLocalOnly { get; }
-
- string[] GetRoles();
-
- bool IgnoreLegacyAuth { get; }
- }
-}
diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs
index 2055a656a..04b2e13e8 100644
--- a/MediaBrowser.Controller/Net/IAuthService.cs
+++ b/MediaBrowser.Controller/Net/IAuthService.cs
@@ -1,7 +1,5 @@
#nullable enable
-using Jellyfin.Data.Entities;
-using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
@@ -11,21 +9,6 @@ namespace MediaBrowser.Controller.Net
///
public interface IAuthService
{
- ///
- /// Authenticate and authorize request.
- ///
- /// Request.
- /// Authorization attributes.
- void Authenticate(IRequest request, IAuthenticationAttributes authAttribtutes);
-
- ///
- /// Authenticate and authorize request.
- ///
- /// Request.
- /// Authorization attributes.
- /// Authenticated user.
- User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtutes);
-
///
/// Authenticate request.
///
diff --git a/MediaBrowser.Controller/Net/IAuthorizationContext.cs b/MediaBrowser.Controller/Net/IAuthorizationContext.cs
index 37a7425b9..4d2f5f5e3 100644
--- a/MediaBrowser.Controller/Net/IAuthorizationContext.cs
+++ b/MediaBrowser.Controller/Net/IAuthorizationContext.cs
@@ -1,4 +1,3 @@
-using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
@@ -20,7 +19,7 @@ namespace MediaBrowser.Controller.Net
///
/// The request context.
/// AuthorizationInfo.
- AuthorizationInfo GetAuthorizationInfo(IRequest requestContext);
+ AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext);
///
/// Gets the authorization information.
diff --git a/MediaBrowser.Controller/Net/IHasResultFactory.cs b/MediaBrowser.Controller/Net/IHasResultFactory.cs
deleted file mode 100644
index b8cf8cd78..000000000
--- a/MediaBrowser.Controller/Net/IHasResultFactory.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using MediaBrowser.Model.Services;
-
-namespace MediaBrowser.Controller.Net
-{
- ///
- /// Interface IHasResultFactory
- /// Services that require a ResultFactory should implement this
- ///
- public interface IHasResultFactory : IRequiresRequest
- {
- ///
- /// Gets or sets the result factory.
- ///
- /// The result factory.
- IHttpResultFactory ResultFactory { get; set; }
- }
-}
diff --git a/MediaBrowser.Controller/Net/IHttpResultFactory.cs b/MediaBrowser.Controller/Net/IHttpResultFactory.cs
deleted file mode 100644
index 8293a8714..000000000
--- a/MediaBrowser.Controller/Net/IHttpResultFactory.cs
+++ /dev/null
@@ -1,82 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Services;
-
-namespace MediaBrowser.Controller.Net
-{
- ///
- /// Interface IHttpResultFactory.
- ///
- public interface IHttpResultFactory
- {
- ///
- /// Gets the result.
- ///
- /// The content.
- /// Type of the content.
- /// The response headers.
- /// System.Object.
- object GetResult(string content, string contentType, IDictionary responseHeaders = null);
-
- object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary responseHeaders = null);
-
- object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary responseHeaders = null);
-
- object GetResult(IRequest requestContext, string content, string contentType, IDictionary responseHeaders = null);
-
- object GetRedirectResult(string url);
-
- object GetResult(IRequest requestContext, T result, IDictionary responseHeaders = null)
- where T : class;
-
- ///
- /// Gets the static result.
- ///
- /// The request context.
- /// The cache key.
- /// The last date modified.
- /// Duration of the cache.
- /// Type of the content.
- /// The factory fn.
- /// The response headers.
- /// if set to true [is head request].
- /// System.Object.
- Task GetStaticResult(IRequest requestContext,
- Guid cacheKey,
- DateTime? lastDateModified,
- TimeSpan? cacheDuration,
- string contentType, Func> factoryFn,
- IDictionary responseHeaders = null,
- bool isHeadRequest = false);
-
- ///
- /// Gets the static result.
- ///
- /// The request context.
- /// The options.
- /// System.Object.
- Task GetStaticResult(IRequest requestContext, StaticResultOptions options);
-
- ///
- /// Gets the static file result.
- ///
- /// The request context.
- /// The path.
- /// The file share.
- /// System.Object.
- Task GetStaticFileResult(IRequest requestContext, string path, FileShare fileShare = FileShare.Read);
-
- ///
- /// Gets the static file result.
- ///
- /// The request context.
- /// The options.
- /// System.Object.
- Task GetStaticFileResult(IRequest requestContext,
- StaticFileResultOptions options);
- }
-}
diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs
index b04ebda8c..845f27045 100644
--- a/MediaBrowser.Controller/Net/IHttpServer.cs
+++ b/MediaBrowser.Controller/Net/IHttpServer.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
-using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
@@ -26,7 +25,7 @@ namespace MediaBrowser.Controller.Net
///
/// Inits this instance.
///
- void Init(IEnumerable serviceTypes, IEnumerable listener, IEnumerable urlPrefixes);
+ void Init(IEnumerable listener, IEnumerable urlPrefixes);
///
/// If set, all requests will respond with this message.
@@ -43,8 +42,8 @@ namespace MediaBrowser.Controller.Net
///
/// Get the default CORS headers.
///
- ///
- ///
- IDictionary GetDefaultCorsHeaders(IRequest req);
+ /// The HTTP context of the current request.
+ /// The default CORS headers for the context.
+ IDictionary GetDefaultCorsHeaders(HttpContext httpContext);
}
}
diff --git a/MediaBrowser.Controller/Net/ISessionContext.cs b/MediaBrowser.Controller/Net/ISessionContext.cs
index 5da748f41..a60dc2ea1 100644
--- a/MediaBrowser.Controller/Net/ISessionContext.cs
+++ b/MediaBrowser.Controller/Net/ISessionContext.cs
@@ -2,7 +2,7 @@
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
{
@@ -12,8 +12,8 @@ namespace MediaBrowser.Controller.Net
User GetUser(object requestContext);
- SessionInfo GetSession(IRequest requestContext);
+ SessionInfo GetSession(HttpContext requestContext);
- User GetUser(IRequest requestContext);
+ User GetUser(HttpContext requestContext);
}
}
diff --git a/MediaBrowser.Controller/Net/StaticResultOptions.cs b/MediaBrowser.Controller/Net/StaticResultOptions.cs
deleted file mode 100644
index c1e9bc845..000000000
--- a/MediaBrowser.Controller/Net/StaticResultOptions.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Net
-{
- public class StaticResultOptions
- {
- public string ContentType { get; set; }
-
- public TimeSpan? CacheDuration { get; set; }
-
- public DateTime? DateLastModified { get; set; }
-
- public Func> ContentFactory { get; set; }
-
- public bool IsHeadRequest { get; set; }
-
- public IDictionary ResponseHeaders { get; set; }
-
- public Action OnComplete { get; set; }
-
- public Action OnError { get; set; }
-
- public string Path { get; set; }
-
- public long? ContentLength { get; set; }
-
- public FileShare FileShare { get; set; }
-
- public StaticResultOptions()
- {
- ResponseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase);
- FileShare = FileShare.Read;
- }
- }
-
- public class StaticFileResultOptions : StaticResultOptions
- {
- }
-}
diff --git a/MediaBrowser.Model/Services/ApiMemberAttribute.cs b/MediaBrowser.Model/Services/ApiMemberAttribute.cs
deleted file mode 100644
index 63f3ecd55..000000000
--- a/MediaBrowser.Model/Services/ApiMemberAttribute.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-#nullable disable
-using System;
-
-namespace MediaBrowser.Model.Services
-{
- ///
- /// Identifies a single API endpoint.
- ///
- [AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
- public class ApiMemberAttribute : Attribute
- {
- ///
- /// Gets or sets verb to which applies attribute. By default applies to all verbs.
- ///
- public string Verb { get; set; }
-
- ///
- /// Gets or sets parameter type: It can be only one of the following: path, query, body, form, or header.
- ///
- public string ParameterType { get; set; }
-
- ///
- /// Gets or sets unique name for the parameter. Each name must be unique, even if they are associated with different paramType values.
- ///
- ///
- ///
- /// Other notes on the name field:
- /// If paramType is body, the name is used only for UI and codegeneration.
- /// If paramType is path, the name field must correspond to the associated path segment from the path field in the api object.
- /// If paramType is query, the name field corresponds to the query param name.
- ///
- ///
- public string Name { get; set; }
-
- ///
- /// Gets or sets the human-readable description for the parameter.
- ///
- public string Description { get; set; }
-
- ///
- /// For path, query, and header paramTypes, this field must be a primitive. For body, this can be a complex or container datatype.
- ///
- public string DataType { get; set; }
-
- ///
- /// For path, this is always true. Otherwise, this field tells the client whether or not the field must be supplied.
- ///
- public bool IsRequired { get; set; }
-
- ///
- /// For query params, this specifies that a comma-separated list of values can be passed to the API. For path and body types, this field cannot be true.
- ///
- public bool AllowMultiple { get; set; }
-
- ///
- /// Gets or sets route to which applies attribute, matches using StartsWith. By default applies to all routes.
- ///
- public string Route { get; set; }
-
- ///
- /// Whether to exclude this property from being included in the ModelSchema.
- ///
- public bool ExcludeInSchema { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Services/IAsyncStreamWriter.cs b/MediaBrowser.Model/Services/IAsyncStreamWriter.cs
deleted file mode 100644
index afbca78a2..000000000
--- a/MediaBrowser.Model/Services/IAsyncStreamWriter.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-#pragma warning disable CS1591
-
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Model.Services
-{
- public interface IAsyncStreamWriter
- {
- Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken);
- }
-}
diff --git a/MediaBrowser.Model/Services/IHasHeaders.cs b/MediaBrowser.Model/Services/IHasHeaders.cs
deleted file mode 100644
index 313f34b41..000000000
--- a/MediaBrowser.Model/Services/IHasHeaders.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Model.Services
-{
- public interface IHasHeaders
- {
- IDictionary Headers { get; }
- }
-}
diff --git a/MediaBrowser.Model/Services/IHasRequestFilter.cs b/MediaBrowser.Model/Services/IHasRequestFilter.cs
deleted file mode 100644
index b83d3b075..000000000
--- a/MediaBrowser.Model/Services/IHasRequestFilter.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-#pragma warning disable CS1591
-
-using Microsoft.AspNetCore.Http;
-
-namespace MediaBrowser.Model.Services
-{
- public interface IHasRequestFilter
- {
- ///
- /// Gets the order in which Request Filters are executed.
- /// <0 Executed before global request filters.
- /// >0 Executed after global request filters.
- ///
- int Priority { get; }
-
- ///
- /// The request filter is executed before the service.
- ///
- /// The http request wrapper.
- /// The http response wrapper.
- /// The request DTO.
- void RequestFilter(IRequest req, HttpResponse res, object requestDto);
- }
-}
diff --git a/MediaBrowser.Model/Services/IHttpRequest.cs b/MediaBrowser.Model/Services/IHttpRequest.cs
deleted file mode 100644
index 3ea65195c..000000000
--- a/MediaBrowser.Model/Services/IHttpRequest.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Services
-{
- public interface IHttpRequest : IRequest
- {
- ///
- /// Gets the HTTP Verb.
- ///
- string HttpMethod { get; }
-
- ///
- /// Gets the value of the Accept HTTP Request Header.
- ///
- string Accept { get; }
- }
-}
diff --git a/MediaBrowser.Model/Services/IHttpResult.cs b/MediaBrowser.Model/Services/IHttpResult.cs
deleted file mode 100644
index abc581d8e..000000000
--- a/MediaBrowser.Model/Services/IHttpResult.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System.Net;
-
-namespace MediaBrowser.Model.Services
-{
- public interface IHttpResult : IHasHeaders
- {
- ///
- /// The HTTP Response Status.
- ///
- int Status { get; set; }
-
- ///
- /// The HTTP Response Status Code.
- ///
- HttpStatusCode StatusCode { get; set; }
-
- ///
- /// The HTTP Response ContentType.
- ///
- string ContentType { get; set; }
-
- ///
- /// Response DTO.
- ///
- object Response { get; set; }
-
- ///
- /// Holds the request call context.
- ///
- IRequest RequestContext { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Services/IRequest.cs b/MediaBrowser.Model/Services/IRequest.cs
deleted file mode 100644
index 8bc1d3668..000000000
--- a/MediaBrowser.Model/Services/IRequest.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using Microsoft.AspNetCore.Http;
-
-namespace MediaBrowser.Model.Services
-{
- public interface IRequest
- {
- HttpResponse Response { get; }
-
- ///
- /// The name of the service being called (e.g. Request DTO Name)
- ///
- string OperationName { get; set; }
-
- ///
- /// The Verb / HttpMethod or Action for this request
- ///
- string Verb { get; }
-
- ///
- /// The request ContentType.
- ///
- string ContentType { get; }
-
- bool IsLocal { get; }
-
- string UserAgent { get; }
-
- ///
- /// The expected Response ContentType for this request.
- ///
- string ResponseContentType { get; set; }
-
- ///
- /// Attach any data to this request that all filters and services can access.
- ///
- Dictionary Items { get; }
-
- IHeaderDictionary Headers { get; }
-
- IQueryCollection QueryString { get; }
-
- string RawUrl { get; }
-
- string AbsoluteUri { get; }
-
- ///
- /// The Remote Ip as reported by X-Forwarded-For, X-Real-IP or Request.UserHostAddress
- ///
- string RemoteIp { get; }
-
- ///
- /// The value of the Authorization Header used to send the Api Key, null if not available.
- ///
- string Authorization { get; }
-
- string[] AcceptTypes { get; }
-
- string PathInfo { get; }
-
- Stream InputStream { get; }
-
- long ContentLength { get; }
-
- ///
- /// The value of the Referrer, null if not available.
- ///
- Uri UrlReferrer { get; }
- }
-
- public interface IHttpFile
- {
- string Name { get; }
-
- string FileName { get; }
-
- long ContentLength { get; }
-
- string ContentType { get; }
-
- Stream InputStream { get; }
- }
-
- public interface IRequiresRequest
- {
- IRequest Request { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Services/IRequiresRequestStream.cs b/MediaBrowser.Model/Services/IRequiresRequestStream.cs
deleted file mode 100644
index 3e5f2da42..000000000
--- a/MediaBrowser.Model/Services/IRequiresRequestStream.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-#pragma warning disable CS1591
-
-using System.IO;
-
-namespace MediaBrowser.Model.Services
-{
- public interface IRequiresRequestStream
- {
- ///
- /// The raw Http Request Input Stream.
- ///
- Stream RequestStream { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Services/IService.cs b/MediaBrowser.Model/Services/IService.cs
deleted file mode 100644
index 5233f57ab..000000000
--- a/MediaBrowser.Model/Services/IService.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Services
-{
- // marker interface
- public interface IService
- {
- }
-
- public interface IReturn { }
-
- public interface IReturn : IReturn { }
-
- public interface IReturnVoid : IReturn { }
-}
diff --git a/MediaBrowser.Model/Services/IStreamWriter.cs b/MediaBrowser.Model/Services/IStreamWriter.cs
deleted file mode 100644
index 3ebfef66b..000000000
--- a/MediaBrowser.Model/Services/IStreamWriter.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-using System.IO;
-
-namespace MediaBrowser.Model.Services
-{
- public interface IStreamWriter
- {
- void WriteTo(Stream responseStream);
- }
-}
diff --git a/MediaBrowser.Model/Services/QueryParamCollection.cs b/MediaBrowser.Model/Services/QueryParamCollection.cs
deleted file mode 100644
index bdb0cabdf..000000000
--- a/MediaBrowser.Model/Services/QueryParamCollection.cs
+++ /dev/null
@@ -1,147 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Model.Dto;
-
-namespace MediaBrowser.Model.Services
-{
- // Remove this garbage class, it's just a bastard copy of NameValueCollection
- public class QueryParamCollection : List
- {
- public QueryParamCollection()
- {
- }
-
- private static StringComparison GetStringComparison()
- {
- return StringComparison.OrdinalIgnoreCase;
- }
-
- ///
- /// Adds a new query parameter.
- ///
- public void Add(string key, string value)
- {
- Add(new NameValuePair(key, value));
- }
-
- private void Set(string key, string value)
- {
- if (string.IsNullOrEmpty(value))
- {
- var parameters = GetItems(key);
-
- foreach (var p in parameters)
- {
- Remove(p);
- }
-
- return;
- }
-
- foreach (var pair in this)
- {
- var stringComparison = GetStringComparison();
-
- if (string.Equals(key, pair.Name, stringComparison))
- {
- pair.Value = value;
- return;
- }
- }
-
- Add(key, value);
- }
-
- private string Get(string name)
- {
- var stringComparison = GetStringComparison();
-
- foreach (var pair in this)
- {
- if (string.Equals(pair.Name, name, stringComparison))
- {
- return pair.Value;
- }
- }
-
- return null;
- }
-
- private List GetItems(string name)
- {
- var stringComparison = GetStringComparison();
-
- var list = new List();
-
- foreach (var pair in this)
- {
- if (string.Equals(pair.Name, name, stringComparison))
- {
- list.Add(pair);
- }
- }
-
- return list;
- }
-
- public virtual List GetValues(string name)
- {
- var stringComparison = GetStringComparison();
-
- var list = new List();
-
- foreach (var pair in this)
- {
- if (string.Equals(pair.Name, name, stringComparison))
- {
- list.Add(pair.Value);
- }
- }
-
- return list;
- }
-
- public IEnumerable Keys
- {
- get
- {
- var keys = new string[this.Count];
-
- for (var i = 0; i < keys.Length; i++)
- {
- keys[i] = this[i].Name;
- }
-
- return keys;
- }
- }
-
- ///
- /// Gets or sets a query parameter value by name. A query may contain multiple values of the same name
- /// (i.e. "x=1&x=2"), in which case the value is an array, which works for both getting and setting.
- ///
- /// The query parameter name.
- /// The query parameter value or array of values.
- public string this[string name]
- {
- get => Get(name);
- set => Set(name, value);
- }
-
- private string GetQueryStringValue(NameValuePair pair)
- {
- return pair.Name + "=" + pair.Value;
- }
-
- public override string ToString()
- {
- var vals = this.Select(GetQueryStringValue).ToArray();
-
- return string.Join("&", vals);
- }
- }
-}
diff --git a/MediaBrowser.Model/Services/RouteAttribute.cs b/MediaBrowser.Model/Services/RouteAttribute.cs
deleted file mode 100644
index f8bf51112..000000000
--- a/MediaBrowser.Model/Services/RouteAttribute.cs
+++ /dev/null
@@ -1,163 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Services
-{
- [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
- public class RouteAttribute : Attribute
- {
- ///
- /// Initializes an instance of the class.
- ///
- ///
- /// The path template to map to the request. See
- /// RouteAttribute.Path
- /// for details on the correct format.
- ///
- public RouteAttribute(string path)
- : this(path, null)
- {
- }
-
- ///
- /// Initializes an instance of the class.
- ///
- ///
- /// The path template to map to the request. See
- /// RouteAttribute.Path
- /// for details on the correct format.
- ///
- /// A comma-delimited list of HTTP verbs supported by the
- /// service. If unspecified, all verbs are assumed to be supported.
- public RouteAttribute(string path, string verbs)
- {
- Path = path;
- Verbs = verbs;
- }
-
- ///
- /// Gets or sets the path template to be mapped to the request.
- ///
- ///
- /// A value providing the path mapped to
- /// the request. Never .
- ///
- ///
- /// Some examples of valid paths are:
- ///
- ///
- /// - "/Inventory"
- /// - "/Inventory/{Category}/{ItemId}"
- /// - "/Inventory/{ItemPath*}"
- ///
- ///
- /// Variables are specified within "{}"
- /// brackets. Each variable in the path is mapped to the same-named property
- /// on the request DTO. At runtime, ServiceStack will parse the
- /// request URL, extract the variable values, instantiate the request DTO,
- /// and assign the variable values into the corresponding request properties,
- /// prior to passing the request DTO to the service object for processing.
- ///
- /// It is not necessary to specify all request properties as
- /// variables in the path. For unspecified properties, callers may provide
- /// values in the query string. For example: the URL
- /// "http://services/Inventory?Category=Books&ItemId=12345" causes the same
- /// request DTO to be processed as "http://services/Inventory/Books/12345",
- /// provided that the paths "/Inventory" (which supports the first URL) and
- /// "/Inventory/{Category}/{ItemId}" (which supports the second URL)
- /// are both mapped to the request DTO.
- ///
- /// Please note that while it is possible to specify property values
- /// in the query string, it is generally considered to be less RESTful and
- /// less desirable than to specify them as variables in the path. Using the
- /// query string to specify property values may also interfere with HTTP
- /// caching.
- ///
- /// The final variable in the path may contain a "*" suffix
- /// to grab all remaining segments in the path portion of the request URL and assign
- /// them to a single property on the request DTO.
- /// For example, if the path "/Inventory/{ItemPath*}" is mapped to the request DTO,
- /// then the request URL "http://services/Inventory/Books/12345" will result
- /// in a request DTO whose ItemPath property contains "Books/12345".
- /// You may only specify one such variable in the path, and it must be positioned at
- /// the end of the path.
- ///
- public string Path { get; set; }
-
- ///
- /// Gets or sets short summary of what the route does.
- ///
- public string Summary { get; set; }
-
- public string Description { get; set; }
-
- public bool IsHidden { get; set; }
-
- ///
- /// Gets or sets longer text to explain the behaviour of the route.
- ///
- public string Notes { get; set; }
-
- ///
- /// Gets or sets a comma-delimited list of HTTP verbs supported by the service, such as
- /// "GET,PUT,POST,DELETE".
- ///
- ///
- /// A providing a comma-delimited list of HTTP verbs supported
- /// by the service, or empty if all verbs are supported.
- ///
- public string Verbs { get; set; }
-
- ///
- /// Used to rank the precedences of route definitions in reverse routing.
- /// i.e. Priorities below 0 are auto-generated have less precedence.
- ///
- public int Priority { get; set; }
-
- protected bool Equals(RouteAttribute other)
- {
- return base.Equals(other)
- && string.Equals(Path, other.Path)
- && string.Equals(Summary, other.Summary)
- && string.Equals(Notes, other.Notes)
- && string.Equals(Verbs, other.Verbs)
- && Priority == other.Priority;
- }
-
- public override bool Equals(object obj)
- {
- if (ReferenceEquals(null, obj))
- {
- return false;
- }
-
- if (ReferenceEquals(this, obj))
- {
- return true;
- }
-
- if (obj.GetType() != this.GetType())
- {
- return false;
- }
-
- return Equals((RouteAttribute)obj);
- }
-
- public override int GetHashCode()
- {
- unchecked
- {
- var hashCode = base.GetHashCode();
- hashCode = (hashCode * 397) ^ (Path != null ? Path.GetHashCode() : 0);
- hashCode = (hashCode * 397) ^ (Summary != null ? Summary.GetHashCode() : 0);
- hashCode = (hashCode * 397) ^ (Notes != null ? Notes.GetHashCode() : 0);
- hashCode = (hashCode * 397) ^ (Verbs != null ? Verbs.GetHashCode() : 0);
- hashCode = (hashCode * 397) ^ Priority;
- return hashCode;
- }
- }
- }
-}
diff --git a/MediaBrowser.Model/Session/PlayRequest.cs b/MediaBrowser.Model/Session/PlayRequest.cs
index eeb25c2e8..6a66465a2 100644
--- a/MediaBrowser.Model/Session/PlayRequest.cs
+++ b/MediaBrowser.Model/Session/PlayRequest.cs
@@ -2,7 +2,6 @@
#pragma warning disable CS1591
using System;
-using MediaBrowser.Model.Services;
namespace MediaBrowser.Model.Session
{
diff --git a/tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs b/tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs
deleted file mode 100644
index 39bd94b59..000000000
--- a/tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using Emby.Server.Implementations.HttpServer;
-using Xunit;
-
-namespace Jellyfin.Server.Implementations.Tests.HttpServer
-{
- public class ResponseFilterTests
- {
- [Theory]
- [InlineData(null, null)]
- [InlineData("", "")]
- [InlineData("This is a clean string.", "This is a clean string.")]
- [InlineData("This isn't \n\ra clean string.", "This isn't a clean string.")]
- public void RemoveControlCharacters_ValidArgs_Correct(string? input, string? result)
- {
- Assert.Equal(result, ResponseFilter.RemoveControlCharacters(input));
- }
- }
-}
--
cgit v1.2.3
From 12710cdf423c038c076c8bd351706e73f3a27899 Mon Sep 17 00:00:00 2001
From: Claus Vium
Date: Wed, 2 Sep 2020 13:06:14 +0200
Subject: More fixes
---
Emby.Server.Implementations/HttpServer/HttpListenerHost.cs | 14 --------------
.../HttpServer/Security/AuthorizationContext.cs | 5 -----
MediaBrowser.Controller/Net/IAuthorizationContext.cs | 7 -------
MediaBrowser.Controller/Net/IHttpServer.cs | 2 +-
4 files changed, 1 insertion(+), 27 deletions(-)
(limited to 'MediaBrowser.Controller')
diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
index 30cb7dd3a..acd7e67db 100644
--- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
+++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
@@ -79,20 +79,6 @@ namespace Emby.Server.Implementations.HttpServer
public string GlobalResponse { get; set; }
- 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;
- }
- }
-
private static Exception GetActualException(Exception ex)
{
if (ex is AggregateException agg)
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
index eec8ac486..4b407dd9d 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
@@ -23,11 +23,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
_userManager = userManager;
}
- public AuthorizationInfo GetAuthorizationInfo(object requestContext)
- {
- return GetAuthorizationInfo((HttpContext)requestContext);
- }
-
public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext)
{
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
diff --git a/MediaBrowser.Controller/Net/IAuthorizationContext.cs b/MediaBrowser.Controller/Net/IAuthorizationContext.cs
index 4d2f5f5e3..0d310548d 100644
--- a/MediaBrowser.Controller/Net/IAuthorizationContext.cs
+++ b/MediaBrowser.Controller/Net/IAuthorizationContext.cs
@@ -7,13 +7,6 @@ namespace MediaBrowser.Controller.Net
///
public interface IAuthorizationContext
{
- ///
- /// Gets the authorization information.
- ///
- /// The request context.
- /// AuthorizationInfo.
- AuthorizationInfo GetAuthorizationInfo(object requestContext);
-
///
/// Gets the authorization information.
///
diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs
index 845f27045..637dd2be3 100644
--- a/MediaBrowser.Controller/Net/IHttpServer.cs
+++ b/MediaBrowser.Controller/Net/IHttpServer.cs
@@ -42,7 +42,7 @@ namespace MediaBrowser.Controller.Net
///
/// Get the default CORS headers.
///
- /// The HTTP context of the current request.
+ /// The HTTP context of the current request.
/// The default CORS headers for the context.
IDictionary GetDefaultCorsHeaders(HttpContext httpContext);
}
--
cgit v1.2.3
From 208b00fbb17d7744cdb632323dc3dbb8099b5cca Mon Sep 17 00:00:00 2001
From: Erwin de Haan
Date: Wed, 2 Sep 2020 20:11:31 +0200
Subject: Add the item path to the ItemLookupInfo class.
This is important for the Shoko Anime Meatdata provider plugin.
---
MediaBrowser.Controller/Entities/BaseItem.cs | 1 +
MediaBrowser.Controller/Providers/ItemLookupInfo.cs | 6 ++++++
2 files changed, 7 insertions(+)
(limited to 'MediaBrowser.Controller')
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 24978d8dd..a5c22e50f 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -2633,6 +2633,7 @@ namespace MediaBrowser.Controller.Entities
{
return new T
{
+ Path = Path,
MetadataCountryCode = GetPreferredMetadataCountryCode(),
MetadataLanguage = GetPreferredMetadataLanguage(),
Name = GetNameForMetadataLookup(),
diff --git a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
index 49974c2a3..b777cc1d3 100644
--- a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
+++ b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
@@ -14,6 +14,12 @@ namespace MediaBrowser.Controller.Providers
/// The name.
public string Name { get; set; }
+ ///
+ /// Gets or sets the path.
+ ///
+ /// The path.
+ public string Path { get; set; }
+
///
/// Gets or sets the metadata language.
///
--
cgit v1.2.3
From 346581bb2ab8e5c93d83e0a486d3d705b54c7d08 Mon Sep 17 00:00:00 2001
From: Erwin de Haan
Date: Wed, 2 Sep 2020 22:42:00 +0200
Subject: Fixes for CI Nuget package pushing and CI triggers
Also adds SourceLink support and symbols that are pushed to Nuget.
Add symbols to main nuget packages for unstable builds (Azure Artifacts does not support symbols outside of the main package)
SourceLink will enable stepping during debugging.
---
.ci/azure-pipelines-package.yml | 36 +++++++++++++++++-----
.ci/azure-pipelines.yml | 14 ++++++---
Emby.Naming/Emby.Naming.csproj | 13 ++++++++
Jellyfin.Data/Jellyfin.Data.csproj | 9 ++++++
MediaBrowser.Common/MediaBrowser.Common.csproj | 10 ++++++
.../MediaBrowser.Controller.csproj | 10 ++++++
MediaBrowser.Model/MediaBrowser.Model.csproj | 10 ++++++
7 files changed, 91 insertions(+), 11 deletions(-)
(limited to 'MediaBrowser.Controller')
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index cfe76d463..2d83320b5 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -138,14 +138,14 @@ jobs:
commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
- task: SSH@0
- displayName: 'Update Stable Repository'
+ displayName: 'Update Stable Repository'
continueOnError: true
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
inputs:
sshEndpoint: repository
runOptions: 'commands'
commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
-
+
- job: PublishNuget
displayName: 'Publish NuGet packages'
dependsOn:
@@ -175,7 +175,7 @@ jobs:
MediaBrowser.Model/MediaBrowser.Model.csproj
Emby.Naming/Emby.Naming.csproj
custom: 'pack'
- arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory)'
+ arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
- task: PublishBuildArtifacts@1
displayName: 'Publish Nuget packages'
@@ -183,10 +183,32 @@ jobs:
pathToPublish: $(Build.ArtifactStagingDirectory)
artifactName: Jellyfin Nuget Packages
+ - task: NuGetAuthenticate@0
+ displayName: 'Authenticate to stable Nuget feed'
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+ inputs:
+ nuGetServiceConnections: 'NugetOrg'
+
- task: NuGetCommand@2
- displayName: 'Push Nuget packages to feed'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
+ displayName: 'Push Nuget packages to stable feed'
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+ inputs:
+ command: 'push'
+ packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg'
+ nuGetFeedType: 'external'
+ publishFeedCredentials: 'NugetOrg'
+ allowPackageConflicts: true # This ignores an error if the version already exists
+
+ - task: NuGetAuthenticate@0
+ displayName: 'Authenticate to unstable Nuget feed'
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
+
+ - task: NuGetCommand@2
+ displayName: 'Push Nuget packages to unstable feed'
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
command: 'push'
- packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
- includeNugetOrg: 'true'
+ packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;!$(Build.ArtifactStagingDirectory)/**/*.symbols.nupkg' # No symbols since Azure Artifact does not support it
+ nuGetFeedType: 'internal'
+ publishVstsFeed: '7cce6c46-d610-45e3-9fb7-65a6bfd1b671/a5746b79-f369-42db-93ff-59cd066f9327'
+ allowPackageConflicts: true # This ignores an error if the version already exists
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index 0c86c0171..6f5f3ff2b 100644
--- a/.ci/azure-pipelines.yml
+++ b/.ci/azure-pipelines.yml
@@ -13,15 +13,21 @@ pr:
trigger:
batch: true
+ branches:
+ include:
+ - '*'
+ tags:
+ include:
+ - 'v*'
jobs:
-- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
+- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- template: azure-pipelines-main.yml
parameters:
LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: $(RestoreBuildProjects)
-- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
+- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- template: azure-pipelines-test.yml
parameters:
ImageNames:
@@ -29,7 +35,7 @@ jobs:
Windows: 'windows-latest'
macOS: 'macos-latest'
-- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
+- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- template: azure-pipelines-abi.yml
parameters:
Packages:
@@ -47,5 +53,5 @@ jobs:
AssemblyFileName: MediaBrowser.Common.dll
LinuxImage: 'ubuntu-latest'
-- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
+- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-package.yml
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index 5e2c6e3e3..6857f9952 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -10,6 +10,15 @@
false
true
true
+ true
+ true
+ true
+ snupkg
+
+
+
+
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
@@ -28,6 +37,10 @@
GPL-3.0-only
+
+
+
+
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index e8065419d..547771faa 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -5,6 +5,15 @@
false
true
true
+ true
+ true
+ true
+ snupkg
+
+
+
+
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index deb674e45..6e258371c 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -18,6 +18,7 @@
+
@@ -32,6 +33,15 @@
false
true
true
+ true
+ true
+ true
+ snupkg
+
+
+
+
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index df92eda38..3674181b1 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -14,6 +14,7 @@
+
@@ -32,6 +33,15 @@
false
true
true
+ true
+ true
+ true
+ snupkg
+
+
+
+
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 0491c9072..4ae38ade9 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -20,9 +20,19 @@
true
enable
latest
+ true
+ true
+ true
+ snupkg
+
+
+
+
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
+
--
cgit v1.2.3
From 5813f8073c5bbe67a6071aed7084adc68f38fd48 Mon Sep 17 00:00:00 2001
From: Claus Vium
Date: Thu, 3 Sep 2020 00:31:42 +0200
Subject: Move HttpListenerHost middleware up the pipeline
---
Emby.Server.Implementations/ApplicationHost.cs | 2 +-
.../HttpServer/HttpListenerHost.cs | 348 +++------------------
Jellyfin.Server/Startup.cs | 30 +-
.../Extensions/HttpContextExtensions.cs | 2 +-
MediaBrowser.Controller/Net/IHttpServer.cs | 7 +-
5 files changed, 78 insertions(+), 311 deletions(-)
(limited to 'MediaBrowser.Controller')
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 4f47d1999..5ed0ad415 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -501,7 +501,7 @@ namespace Emby.Server.Implementations
}
public Task ExecuteHttpHandlerAsync(HttpContext context, Func next)
- => _httpServer.RequestHandler(context);
+ => _httpServer.RequestHandler(context, next);
///
/// Registers services/resources with the service collection that will be available via DI.
diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
index 4165cdb96..27369960b 100644
--- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
+++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
@@ -2,26 +2,17 @@
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.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
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.Globalization;
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;
@@ -39,32 +30,25 @@ namespace Emby.Server.Implementations.HttpServer
private readonly ILoggerFactory _loggerFactory;
private readonly IServerConfigurationManager _config;
private readonly INetworkManager _networkManager;
- private readonly IServerApplicationHost _appHost;
private readonly string _defaultRedirectPath;
private readonly string _baseUrlPrefix;
- private readonly IHostEnvironment _hostEnvironment;
-
private IWebSocketListener[] _webSocketListeners = Array.Empty();
private bool _disposed = false;
public HttpListenerHost(
- IServerApplicationHost applicationHost,
ILogger logger,
IServerConfigurationManager config,
IConfiguration configuration,
INetworkManager networkManager,
ILocalizationManager localizationManager,
- IHostEnvironment hostEnvironment,
ILoggerFactory loggerFactory)
{
- _appHost = applicationHost;
_logger = logger;
_config = config;
_defaultRedirectPath = configuration[DefaultRedirectKey];
_baseUrlPrefix = _config.Configuration.BaseUrl;
_networkManager = networkManager;
- _hostEnvironment = hostEnvironment;
_loggerFactory = loggerFactory;
Instance = this;
@@ -79,122 +63,6 @@ namespace Emby.Server.Implementations.HttpServer
public string GlobalResponse { get; set; }
- 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, HttpContext httpContext, 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 = httpContext.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('/');
@@ -267,187 +135,90 @@ namespace Emby.Server.Implementations.HttpServer
return true;
}
- ///
- /// Validate a connection from a remote IP address to a URL to see if a redirection to HTTPS is required.
- ///
- /// True if the request is valid, or false if the request is not valid and an HTTPS redirect is required.
- 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;
- }
-
///
- public Task RequestHandler(HttpContext context)
+ public Task RequestHandler(HttpContext context, Func next)
{
if (context.WebSockets.IsWebSocketRequest)
{
return WebSocketRequestHandler(context);
}
- return RequestHandler(context, context.RequestAborted);
+ return HttpRequestHandler(context, next);
}
///
/// Overridable method that can be used to implement a custom handler.
///
- private async Task RequestHandler(HttpContext httpContext, CancellationToken cancellationToken)
+ private async Task HttpRequestHandler(HttpContext httpContext, Func next)
{
- var stopWatch = new Stopwatch();
- stopWatch.Start();
+ var cancellationToken = httpContext.RequestAborted;
var httpRes = httpContext.Response;
var host = httpContext.Request.Host.ToString();
var localPath = httpContext.Request.Path.ToString();
- var urlString = httpContext.Request.GetDisplayUrl();
- string urlToLog = GetUrlToLog(urlString);
string remoteIp = httpContext.Request.RemoteIp();
- try
+ if (_disposed)
{
- 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, httpContext.Request.IsLocal()))
- {
- httpRes.StatusCode = 403;
- httpRes.ContentType = "text/plain";
- await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false);
- return;
- }
-
- if (!ValidateSsl(httpContext.Request.RemoteIp(), urlString))
- {
- RedirectToSecureUrl(httpRes, urlString);
- return;
- }
-
- if (string.Equals(httpContext.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase))
- {
- httpRes.StatusCode = 200;
- foreach (var (key, value) in GetDefaultCorsHeaders(httpContext))
- {
- 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;
- }
+ httpRes.StatusCode = 503;
+ httpRes.ContentType = "text/plain";
+ await httpRes.WriteAsync("Server shutting down", cancellationToken).ConfigureAwait(false);
+ 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;
- }
- }
+ if (!ValidateHost(host))
+ {
+ httpRes.StatusCode = 400;
+ httpRes.ContentType = "text/plain";
+ await httpRes.WriteAsync("Invalid host", cancellationToken).ConfigureAwait(false);
+ return;
+ }
- throw new FileNotFoundException();
+ if (!ValidateRequest(remoteIp, httpContext.Request.IsLocal()))
+ {
+ httpRes.StatusCode = 403;
+ httpRes.ContentType = "text/plain";
+ await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false);
+ return;
}
- catch (Exception requestEx)
+
+ if (string.Equals(httpContext.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase))
{
- try
+ httpRes.StatusCode = 200;
+ foreach (var (key, value) in GetDefaultCorsHeaders(httpContext))
{
- var requestInnerEx = GetActualException(requestEx);
- var statusCode = GetStatusCode(requestInnerEx);
-
- foreach (var (key, value) in GetDefaultCorsHeaders(httpContext))
- {
- 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, httpContext, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false);
+ httpRes.Headers.Add(key, value);
}
- 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;
- }
- }
+ httpRes.ContentType = "text/plain";
+ await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false);
+ return;
}
- finally
+
+ 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))
{
- if (httpRes.StatusCode >= 500)
- {
- _logger.LogDebug("Sending HTTP Response 500 in response to {Url}", urlToLog);
- }
+ // 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;
+ }
- stopWatch.Stop();
- var elapsed = stopWatch.Elapsed;
- if (elapsed.TotalMilliseconds > 500)
+ if (!string.IsNullOrEmpty(GlobalResponse))
+ {
+ // We don't want the address pings in ApplicationHost to fail
+ if (localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1)
{
- _logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog);
+ httpRes.StatusCode = 503;
+ httpRes.ContentType = "text/html";
+ await httpRes.WriteAsync(GlobalResponse, cancellationToken).ConfigureAwait(false);
+ return;
}
}
+
+ await next().ConfigureAwait(false);
}
private async Task WebSocketRequestHandler(HttpContext context)
@@ -508,21 +279,6 @@ namespace Emby.Server.Implementations.HttpServer
return headers;
}
- private void RedirectToSecureUrl(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);
- }
-
///
/// Adds the rest handlers.
///
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index cbc1c040c..81243902a 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -23,17 +23,19 @@ namespace Jellyfin.Server
public class Startup
{
private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly IApplicationHost _applicationHost;
+ private readonly IServerApplicationHost _serverApplicationHost;
///
/// Initializes a new instance of the class.
///
/// The server configuration manager.
- /// The application host.
- public Startup(IServerConfigurationManager serverConfigurationManager, IApplicationHost applicationHost)
+ /// The server application host.
+ public Startup(
+ IServerConfigurationManager serverConfigurationManager,
+ IServerApplicationHost serverApplicationHost)
{
_serverConfigurationManager = serverConfigurationManager;
- _applicationHost = applicationHost;
+ _serverApplicationHost = serverApplicationHost;
}
///
@@ -44,7 +46,9 @@ namespace Jellyfin.Server
{
services.AddResponseCompression();
services.AddHttpContextAccessor();
- services.AddJellyfinApi(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'), _applicationHost.GetApiPluginAssemblies());
+ services.AddJellyfinApi(
+ _serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'),
+ _serverApplicationHost.GetApiPluginAssemblies());
services.AddJellyfinApiSwagger();
@@ -53,7 +57,9 @@ namespace Jellyfin.Server
services.AddJellyfinApiAuthorization();
- var productHeader = new ProductInfoHeaderValue(_applicationHost.Name.Replace(' ', '-'), _applicationHost.ApplicationVersionString);
+ var productHeader = new ProductInfoHeaderValue(
+ _serverApplicationHost.Name.Replace(' ', '-'),
+ _serverApplicationHost.ApplicationVersionString);
services
.AddHttpClient(NamedClient.Default, c =>
{
@@ -64,7 +70,7 @@ namespace Jellyfin.Server
services.AddHttpClient(NamedClient.MusicBrainz, c =>
{
c.DefaultRequestHeaders.UserAgent.Add(productHeader);
- c.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue($"({_applicationHost.ApplicationUserAgentAddress})"));
+ c.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue($"({_serverApplicationHost.ApplicationUserAgentAddress})"));
})
.ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler());
}
@@ -93,7 +99,11 @@ namespace Jellyfin.Server
app.UseResponseCompression();
- // TODO app.UseMiddleware();
+ if (_serverConfigurationManager.Configuration.RequireHttps
+ && _serverApplicationHost.ListenWithHttps)
+ {
+ app.UseHttpsRedirection();
+ }
app.UseAuthentication();
app.UseJellyfinApiSwagger(_serverConfigurationManager);
@@ -106,6 +116,8 @@ namespace Jellyfin.Server
app.UseHttpMetrics();
}
+ app.Use(serverApplicationHost.ExecuteHttpHandlerAsync);
+
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
@@ -115,8 +127,6 @@ namespace Jellyfin.Server
}
});
- app.Use(serverApplicationHost.ExecuteHttpHandlerAsync);
-
// Add type descriptor for legacy datetime parsing.
TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter)));
}
diff --git a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs
index 86c3b3536..e0cf3f9ac 100644
--- a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs
+++ b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs
@@ -28,7 +28,7 @@ namespace MediaBrowser.Common.Extensions
/// The remote caller IP address.
public static string RemoteIp(this HttpRequest request)
{
- var cachedRemoteIp = request.HttpContext.Items["RemoteIp"].ToString();
+ var cachedRemoteIp = request.HttpContext.Items["RemoteIp"]?.ToString();
if (!string.IsNullOrEmpty(cachedRemoteIp))
{
return cachedRemoteIp;
diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs
index 637dd2be3..6739f2fa6 100644
--- a/MediaBrowser.Controller/Net/IHttpServer.cs
+++ b/MediaBrowser.Controller/Net/IHttpServer.cs
@@ -35,9 +35,10 @@ namespace MediaBrowser.Controller.Net
///
/// The HTTP request handler.
///
- ///
- ///
- Task RequestHandler(HttpContext context);
+ /// The current HTTP context.
+ /// The next middleware in the ASP.NET pipeline.
+ /// The task.
+ Task RequestHandler(HttpContext context, Func next);
///
/// Get the default CORS headers.
--
cgit v1.2.3
From 571d0570f5560bde79d21c33173742f6a31e24cf Mon Sep 17 00:00:00 2001
From: Claus Vium
Date: Thu, 3 Sep 2020 11:32:22 +0200
Subject: Kill HttpListenerHost
---
Emby.Server.Implementations/ApplicationHost.cs | 19 +-
.../ConfigurationOptions.cs | 2 +-
.../HttpServer/HttpListenerHost.cs | 315 ---------------------
.../HttpServer/WebSocketManager.cs | 102 +++++++
.../Session/SessionWebSocketListener.cs | 12 +-
.../Extensions/ApiApplicationBuilderExtensions.cs | 61 ++++
.../Middleware/BaseUrlRedirectionMiddleware.cs | 62 ++++
.../Middleware/CorsOptionsResponseMiddleware.cs | 69 +++++
.../IpBasedAccessValidationMiddleware.cs | 76 +++++
.../Middleware/LanFilteringMiddleware.cs | 76 +++++
.../Middleware/ServerStartupMessageMiddleware.cs | 38 +++
.../Middleware/WebSocketHandlerMiddleware.cs | 40 +++
Jellyfin.Server/Program.cs | 4 +-
Jellyfin.Server/Startup.cs | 12 +-
.../Extensions/ConfigurationExtensions.cs | 6 +
MediaBrowser.Controller/IServerApplicationHost.cs | 3 +-
MediaBrowser.Controller/Net/IHttpServer.cs | 50 ----
MediaBrowser.Controller/Net/IWebSocketManager.cs | 32 +++
18 files changed, 589 insertions(+), 390 deletions(-)
delete mode 100644 Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
create mode 100644 Emby.Server.Implementations/HttpServer/WebSocketManager.cs
create mode 100644 Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
create mode 100644 Jellyfin.Server/Middleware/CorsOptionsResponseMiddleware.cs
create mode 100644 Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
create mode 100644 Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
create mode 100644 Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs
create mode 100644 Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs
delete mode 100644 MediaBrowser.Controller/Net/IHttpServer.cs
create mode 100644 MediaBrowser.Controller/Net/IWebSocketManager.cs
(limited to 'MediaBrowser.Controller')
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 5ed0ad415..c8af6b73a 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -96,12 +96,12 @@ using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb;
using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
-using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
+using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
namespace Emby.Server.Implementations
{
@@ -122,9 +122,11 @@ namespace Emby.Server.Implementations
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
- private IHttpServer _httpServer;
+ private IWebSocketManager _webSocketManager;
private IHttpClient _httpClient;
+ private string[] _urlPrefixes;
+
///
/// Gets a value indicating whether this instance can self restart.
///
@@ -444,7 +446,6 @@ namespace Emby.Server.Implementations
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
Logger.LogInformation("Core startup complete");
- _httpServer.GlobalResponse = null;
stopWatch.Restart();
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
@@ -500,9 +501,6 @@ namespace Emby.Server.Implementations
RegisterServices();
}
- public Task ExecuteHttpHandlerAsync(HttpContext context, Func next)
- => _httpServer.RequestHandler(context, next);
-
///
/// Registers services/resources with the service collection that will be available via DI.
///
@@ -577,7 +575,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ ServiceCollection.AddSingleton();
ServiceCollection.AddSingleton();
@@ -650,7 +648,7 @@ namespace Emby.Server.Implementations
_mediaEncoder = Resolve();
_sessionManager = Resolve();
- _httpServer = Resolve();
+ _webSocketManager = Resolve();
_httpClient = Resolve();
((AuthenticationRepository)Resolve()).Initialize();
@@ -771,7 +769,8 @@ namespace Emby.Server.Implementations
.Where(i => i != null)
.ToArray();
- _httpServer.Init(GetExports(), GetUrlPrefixes());
+ _urlPrefixes = GetUrlPrefixes().ToArray();
+ _webSocketManager.Init(GetExports());
Resolve().AddParts(
GetExports(),
@@ -937,7 +936,7 @@ namespace Emby.Server.Implementations
}
}
- if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
+ if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
{
requiresRestart = true;
}
diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs
index 64ccff53b..fde6fa115 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -15,7 +15,7 @@ namespace Emby.Server.Implementations
public static Dictionary DefaultConfiguration => new Dictionary
{
{ HostWebClientKey, bool.TrueString },
- { HttpListenerHost.DefaultRedirectKey, "web/index.html" },
+ { DefaultRedirectKey, "web/index.html" },
{ FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.TrueString },
diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
deleted file mode 100644
index 27369960b..000000000
--- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
+++ /dev/null
@@ -1,315 +0,0 @@
-#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.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Globalization;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Primitives;
-
-namespace Emby.Server.Implementations.HttpServer
-{
- public class HttpListenerHost : IHttpServer
- {
- ///
- /// The key for a setting that specifies the default redirect path
- /// to use for requests where the URL base prefix is invalid or missing.
- ///
- public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
-
- private readonly ILogger _logger;
- private readonly ILoggerFactory _loggerFactory;
- private readonly IServerConfigurationManager _config;
- private readonly INetworkManager _networkManager;
- private readonly string _defaultRedirectPath;
- private readonly string _baseUrlPrefix;
-
- private IWebSocketListener[] _webSocketListeners = Array.Empty();
- private bool _disposed = false;
-
- public HttpListenerHost(
- ILogger logger,
- IServerConfigurationManager config,
- IConfiguration configuration,
- INetworkManager networkManager,
- ILocalizationManager localizationManager,
- ILoggerFactory loggerFactory)
- {
- _logger = logger;
- _config = config;
- _defaultRedirectPath = configuration[DefaultRedirectKey];
- _baseUrlPrefix = _config.Configuration.BaseUrl;
- _networkManager = networkManager;
- _loggerFactory = loggerFactory;
-
- Instance = this;
- GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
- }
-
- public event EventHandler> WebSocketConnected;
-
- public static HttpListenerHost Instance { get; protected set; }
-
- public string[] UrlPrefixes { get; private set; }
-
- public string GlobalResponse { get; set; }
-
- 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;
- }
-
- ///
- public Task RequestHandler(HttpContext context, Func next)
- {
- if (context.WebSockets.IsWebSocketRequest)
- {
- return WebSocketRequestHandler(context);
- }
-
- return HttpRequestHandler(context, next);
- }
-
- ///
- /// Overridable method that can be used to implement a custom handler.
- ///
- private async Task HttpRequestHandler(HttpContext httpContext, Func next)
- {
- var cancellationToken = httpContext.RequestAborted;
- var httpRes = httpContext.Response;
- var host = httpContext.Request.Host.ToString();
- var localPath = httpContext.Request.Path.ToString();
- string remoteIp = httpContext.Request.RemoteIp();
-
- 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, httpContext.Request.IsLocal()))
- {
- httpRes.StatusCode = 403;
- httpRes.ContentType = "text/plain";
- await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false);
- return;
- }
-
- if (string.Equals(httpContext.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase))
- {
- httpRes.StatusCode = 200;
- foreach (var (key, value) in GetDefaultCorsHeaders(httpContext))
- {
- 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;
- }
- }
-
- await next().ConfigureAwait(false);
- }
-
- 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(),
- webSocket,
- context.Connection.RemoteIpAddress,
- context.Request.Query)
- {
- OnReceive = ProcessWebSocketMessageReceived
- };
-
- WebSocketConnected?.Invoke(this, new GenericEventArgs(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;
- }
- }
- }
-
- ///
- public IDictionary GetDefaultCorsHeaders(HttpContext httpContext)
- {
- var origin = httpContext.Request.Headers["Origin"];
- if (origin == StringValues.Empty)
- {
- origin = httpContext.Request.Headers["Host"];
- if (origin == StringValues.Empty)
- {
- origin = "*";
- }
- }
-
- var headers = new Dictionary();
- 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;
- }
-
- ///
- /// Adds the rest handlers.
- ///
- /// The web socket listeners.
- /// The URL prefixes. See .
- public void Init(IEnumerable listeners, IEnumerable urlPrefixes)
- {
- _webSocketListeners = listeners.ToArray();
- UrlPrefixes = urlPrefixes.ToArray();
- }
-
- ///
- /// Processes the web socket message received.
- ///
- /// The result.
- private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
- {
- if (_disposed)
- {
- return Task.CompletedTask;
- }
-
- IEnumerable GetTasks()
- {
- foreach (var x in _webSocketListeners)
- {
- yield return x.ProcessMessageAsync(result);
- }
- }
-
- return Task.WhenAll(GetTasks());
- }
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
new file mode 100644
index 000000000..89c1b7ea0
--- /dev/null
+++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
@@ -0,0 +1,102 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+using Jellyfin.Data.Events;
+using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.HttpServer
+{
+ public class WebSocketManager : IWebSocketManager
+ {
+ private readonly ILogger _logger;
+ private readonly ILoggerFactory _loggerFactory;
+
+ private IWebSocketListener[] _webSocketListeners = Array.Empty();
+ private bool _disposed = false;
+
+ public WebSocketManager(
+ ILogger logger,
+ ILoggerFactory loggerFactory)
+ {
+ _logger = logger;
+ _loggerFactory = loggerFactory;
+ }
+
+ public event EventHandler> WebSocketConnected;
+
+ ///
+ 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(),
+ webSocket,
+ context.Connection.RemoteIpAddress,
+ context.Request.Query)
+ {
+ OnReceive = ProcessWebSocketMessageReceived
+ };
+
+ WebSocketConnected?.Invoke(this, new GenericEventArgs(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;
+ }
+ }
+ }
+
+ ///
+ /// Adds the rest handlers.
+ ///
+ /// The web socket listeners.
+ public void Init(IEnumerable listeners)
+ {
+ _webSocketListeners = listeners.ToArray();
+ }
+
+ ///
+ /// Processes the web socket message received.
+ ///
+ /// The result.
+ private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
+ {
+ if (_disposed)
+ {
+ return Task.CompletedTask;
+ }
+
+ IEnumerable GetTasks()
+ {
+ foreach (var x in _webSocketListeners)
+ {
+ yield return x.ProcessMessageAsync(result);
+ }
+ }
+
+ return Task.WhenAll(GetTasks());
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index 1da7a6473..15c2af220 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Session
private readonly ILogger _logger;
private readonly ILoggerFactory _loggerFactory;
- private readonly IHttpServer _httpServer;
+ private readonly IWebSocketManager _webSocketManager;
///
/// The KeepAlive cancellation token.
@@ -72,19 +72,19 @@ namespace Emby.Server.Implementations.Session
/// The logger.
/// The session manager.
/// The logger factory.
- /// The HTTP server.
+ /// The HTTP server.
public SessionWebSocketListener(
ILogger logger,
ISessionManager sessionManager,
ILoggerFactory loggerFactory,
- IHttpServer httpServer)
+ IWebSocketManager webSocketManager)
{
_logger = logger;
_sessionManager = sessionManager;
_loggerFactory = loggerFactory;
- _httpServer = httpServer;
+ _webSocketManager = webSocketManager;
- httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
+ webSocketManager.WebSocketConnected += OnServerManagerWebSocketConnected;
}
private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs e)
@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.Session
///
public void Dispose()
{
- _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
+ _webSocketManager.WebSocketConnected -= OnServerManagerWebSocketConnected;
StopKeepAlive();
}
diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index 745567703..33a8d7532 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -1,3 +1,4 @@
+using Jellyfin.Server.Middleware;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Builder;
@@ -46,5 +47,65 @@ namespace Jellyfin.Server.Extensions
c.RoutePrefix = $"{baseUrl}api-docs/redoc";
});
}
+
+ ///
+ /// Adds IP based access validation to the application pipeline.
+ ///
+ /// The application builder.
+ /// The updated application builder.
+ public static IApplicationBuilder UseIpBasedAccessValidation(this IApplicationBuilder appBuilder)
+ {
+ return appBuilder.UseMiddleware();
+ }
+
+ ///
+ /// Adds LAN based access filtering to the application pipeline.
+ ///
+ /// The application builder.
+ /// The updated application builder.
+ public static IApplicationBuilder UseLanFiltering(this IApplicationBuilder appBuilder)
+ {
+ return appBuilder.UseMiddleware();
+ }
+
+ ///
+ /// Adds CORS OPTIONS request handling to the application pipeline.
+ ///
+ /// The application builder.
+ /// The updated application builder.
+ public static IApplicationBuilder UseCorsOptionsResponse(this IApplicationBuilder appBuilder)
+ {
+ return appBuilder.UseMiddleware();
+ }
+
+ ///
+ /// Adds base url redirection to the application pipeline.
+ ///
+ /// The application builder.
+ /// The updated application builder.
+ public static IApplicationBuilder UseBaseUrlRedirection(this IApplicationBuilder appBuilder)
+ {
+ return appBuilder.UseMiddleware();
+ }
+
+ ///
+ /// Adds a custom message during server startup to the application pipeline.
+ ///
+ /// The application builder.
+ /// The updated application builder.
+ public static IApplicationBuilder UseServerStartupMessage(this IApplicationBuilder appBuilder)
+ {
+ return appBuilder.UseMiddleware();
+ }
+
+ ///
+ /// Adds a WebSocket request handler to the application pipeline.
+ ///
+ /// The application builder.
+ /// The updated application builder.
+ public static IApplicationBuilder UseWebSocketHandler(this IApplicationBuilder appBuilder)
+ {
+ return appBuilder.UseMiddleware();
+ }
}
}
diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
new file mode 100644
index 000000000..9316737bd
--- /dev/null
+++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions;
+
+namespace Jellyfin.Server.Middleware
+{
+ ///
+ /// Redirect requests without baseurl prefix to the baseurl prefixed URL.
+ ///
+ public class BaseUrlRedirectionMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly ILogger _logger;
+ private readonly IConfiguration _configuration;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The next delegate in the pipeline.
+ /// The logger.
+ /// The application configuration.
+ public BaseUrlRedirectionMiddleware(
+ RequestDelegate next,
+ ILogger logger,
+ IConfiguration configuration)
+ {
+ _next = next;
+ _logger = logger;
+ _configuration = configuration;
+ }
+
+ ///
+ /// Executes the middleware action.
+ ///
+ /// The current HTTP context.
+ /// The server configuration manager.
+ /// The async task.
+ public async Task Invoke(HttpContext httpContext, IServerConfigurationManager serverConfigurationManager)
+ {
+ var localPath = httpContext.Request.Path.ToString();
+ var baseUrlPrefix = serverConfigurationManager.Configuration.BaseUrl;
+
+ 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 an URL at {LocalPath}", localPath);
+ httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]);
+ return;
+ }
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Middleware/CorsOptionsResponseMiddleware.cs b/Jellyfin.Server/Middleware/CorsOptionsResponseMiddleware.cs
new file mode 100644
index 000000000..8214f8907
--- /dev/null
+++ b/Jellyfin.Server/Middleware/CorsOptionsResponseMiddleware.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Mime;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Primitives;
+
+namespace Jellyfin.Server.Middleware
+{
+ ///
+ /// Middleware for handling OPTIONS requests.
+ ///
+ public class CorsOptionsResponseMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The next delegate in the pipeline.
+ public CorsOptionsResponseMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ ///
+ /// Executes the middleware action.
+ ///
+ /// The current HTTP context.
+ /// The async task.
+ public async Task Invoke(HttpContext httpContext)
+ {
+ if (string.Equals(httpContext.Request.Method, HttpMethods.Options, StringComparison.OrdinalIgnoreCase))
+ {
+ httpContext.Response.StatusCode = 200;
+ foreach (var (key, value) in GetDefaultCorsHeaders(httpContext))
+ {
+ httpContext.Response.Headers.Add(key, value);
+ }
+
+ httpContext.Response.ContentType = MediaTypeNames.Text.Plain;
+ await httpContext.Response.WriteAsync(string.Empty, httpContext.RequestAborted).ConfigureAwait(false);
+ return;
+ }
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+
+ private static IDictionary GetDefaultCorsHeaders(HttpContext httpContext)
+ {
+ var origin = httpContext.Request.Headers["Origin"];
+ if (origin == StringValues.Empty)
+ {
+ origin = httpContext.Request.Headers["Host"];
+ if (origin == StringValues.Empty)
+ {
+ origin = "*";
+ }
+ }
+
+ var headers = new Dictionary();
+ 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;
+ }
+ }
+}
diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
new file mode 100644
index 000000000..59b5fb1ed
--- /dev/null
+++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
@@ -0,0 +1,76 @@
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Server.Middleware
+{
+ ///
+ /// Validates the IP of requests coming from local networks wrt. remote access.
+ ///
+ public class IpBasedAccessValidationMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The next delegate in the pipeline.
+ public IpBasedAccessValidationMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ ///
+ /// Executes the middleware action.
+ ///
+ /// The current HTTP context.
+ /// The network manager.
+ /// The server configuration manager.
+ /// The async task.
+ public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager)
+ {
+ if (httpContext.Request.IsLocal())
+ {
+ await _next(httpContext).ConfigureAwait(false);
+ return;
+ }
+
+ var remoteIp = httpContext.Request.RemoteIp();
+
+ if (serverConfigurationManager.Configuration.EnableRemoteAccess)
+ {
+ var addressFilter = serverConfigurationManager.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
+
+ if (addressFilter.Length > 0 && !networkManager.IsInLocalNetwork(remoteIp))
+ {
+ if (serverConfigurationManager.Configuration.IsRemoteIPFilterBlacklist)
+ {
+ if (networkManager.IsAddressInSubnets(remoteIp, addressFilter))
+ {
+ return;
+ }
+ }
+ else
+ {
+ if (!networkManager.IsAddressInSubnets(remoteIp, addressFilter))
+ {
+ return;
+ }
+ }
+ }
+ }
+ else
+ {
+ if (!networkManager.IsInLocalNetwork(remoteIp))
+ {
+ return;
+ }
+ }
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
new file mode 100644
index 000000000..9d795145a
--- /dev/null
+++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Server.Middleware
+{
+ ///
+ /// Validates the LAN host IP based on application configuration.
+ ///
+ public class LanFilteringMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The next delegate in the pipeline.
+ public LanFilteringMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ ///
+ /// Executes the middleware action.
+ ///
+ /// The current HTTP context.
+ /// The network manager.
+ /// The server configuration manager.
+ /// The async task.
+ public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager)
+ {
+ var currentHost = httpContext.Request.Host.ToString();
+ var hosts = serverConfigurationManager
+ .Configuration
+ .LocalNetworkAddresses
+ .Select(NormalizeConfiguredLocalAddress)
+ .ToList();
+
+ if (hosts.Count == 0)
+ {
+ await _next(httpContext).ConfigureAwait(false);
+ return;
+ }
+
+ currentHost ??= string.Empty;
+
+ if (networkManager.IsInPrivateAddressSpace(currentHost))
+ {
+ hosts.Add("localhost");
+ hosts.Add("127.0.0.1");
+
+ if (hosts.All(i => currentHost.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1))
+ {
+ return;
+ }
+ }
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+
+ 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();
+ }
+ }
+}
diff --git a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs b/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs
new file mode 100644
index 000000000..4f347d6d3
--- /dev/null
+++ b/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs
@@ -0,0 +1,38 @@
+using System.Net.Mime;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Globalization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Server.Middleware
+{
+ ///
+ /// Shows a custom message during server startup.
+ ///
+ public class ServerStartupMessageMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The next delegate in the pipeline.
+ public ServerStartupMessageMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ ///
+ /// Executes the middleware action.
+ ///
+ /// The current HTTP context.
+ /// The localization manager.
+ /// The async task.
+ public async Task Invoke(HttpContext httpContext, ILocalizationManager localizationManager)
+ {
+ var message = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
+ httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
+ httpContext.Response.ContentType = MediaTypeNames.Text.Html;
+ await httpContext.Response.WriteAsync(message, httpContext.RequestAborted).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs b/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs
new file mode 100644
index 000000000..b7a5d2b34
--- /dev/null
+++ b/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs
@@ -0,0 +1,40 @@
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Server.Middleware
+{
+ ///
+ /// Handles WebSocket requests.
+ ///
+ public class WebSocketHandlerMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The next delegate in the pipeline.
+ public WebSocketHandlerMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ ///
+ /// Executes the middleware action.
+ ///
+ /// The current HTTP context.
+ /// The WebSocket connection manager.
+ /// The async task.
+ public async Task Invoke(HttpContext httpContext, IWebSocketManager webSocketManager)
+ {
+ if (!httpContext.WebSockets.IsWebSocketRequest)
+ {
+ await _next(httpContext).ConfigureAwait(false);
+ return;
+ }
+
+ await webSocketManager.WebSocketRequestHandler(httpContext).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 14cc5f4c2..b9a90f9db 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -11,7 +11,6 @@ using System.Threading;
using System.Threading.Tasks;
using CommandLine;
using Emby.Server.Implementations;
-using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Networking;
using Jellyfin.Api.Controllers;
@@ -28,6 +27,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using Serilog.Extensions.Logging;
using SQLitePCL;
+using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Jellyfin.Server
@@ -594,7 +594,7 @@ namespace Jellyfin.Server
var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration;
if (startupConfig != null && !startupConfig.HostWebClient())
{
- inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "api-docs/swagger";
+ inMemoryDefaultConfig[ConfigurationExtensions.DefaultRedirectKey] = "api-docs/swagger";
}
return config
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 9316ab79e..80f679420 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -84,11 +84,9 @@ namespace Jellyfin.Server
///
/// The application builder.
/// The webhost environment.
- /// The server application host.
public void Configure(
IApplicationBuilder app,
- IWebHostEnvironment env,
- IServerApplicationHost serverApplicationHost)
+ IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
@@ -120,7 +118,11 @@ namespace Jellyfin.Server
app.UseHttpMetrics();
}
- app.Use(serverApplicationHost.ExecuteHttpHandlerAsync);
+ app.UseLanFiltering();
+ app.UseIpBasedAccessValidation();
+ app.UseCorsOptionsResponse();
+ app.UseBaseUrlRedirection();
+ app.UseWebSocketHandler();
app.UseEndpoints(endpoints =>
{
@@ -131,6 +133,8 @@ namespace Jellyfin.Server
}
});
+ app.UseServerStartupMessage();
+
// Add type descriptor for legacy datetime parsing.
TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter)));
}
diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
index 4c2209b67..f9285c768 100644
--- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
@@ -8,6 +8,12 @@ namespace MediaBrowser.Controller.Extensions
///
public static class ConfigurationExtensions
{
+ ///
+ /// The key for a setting that specifies the default redirect path
+ /// to use for requests where the URL base prefix is invalid or missing..
+ ///
+ public const string DefaultRedirectKey = "DefaultRedirectPath";
+
///
/// The key for a setting that indicates whether the application should host web client content.
///
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index 39b896c0f..d482c19d9 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -117,8 +117,7 @@ namespace MediaBrowser.Controller
IEnumerable GetWakeOnLanInfo();
string ExpandVirtualPath(string path);
- string ReverseVirtualPath(string path);
- Task ExecuteHttpHandlerAsync(HttpContext context, Func next);
+ string ReverseVirtualPath(string path);
}
}
diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs
deleted file mode 100644
index 6739f2fa6..000000000
--- a/MediaBrowser.Controller/Net/IHttpServer.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using Jellyfin.Data.Events;
-using Microsoft.AspNetCore.Http;
-
-namespace MediaBrowser.Controller.Net
-{
- ///
- /// Interface IHttpServer.
- ///
- public interface IHttpServer
- {
- ///
- /// Gets the URL prefix.
- ///
- /// The URL prefix.
- string[] UrlPrefixes { get; }
-
- ///
- /// Occurs when [web socket connected].
- ///
- event EventHandler> WebSocketConnected;
-
- ///
- /// Inits this instance.
- ///
- void Init(IEnumerable listener, IEnumerable urlPrefixes);
-
- ///
- /// If set, all requests will respond with this message.
- ///
- string GlobalResponse { get; set; }
-
- ///
- /// The HTTP request handler.
- ///
- /// The current HTTP context.
- /// The next middleware in the ASP.NET pipeline.
- /// The task.
- Task RequestHandler(HttpContext context, Func next);
-
- ///
- /// Get the default CORS headers.
- ///
- /// The HTTP context of the current request.
- /// The default CORS headers for the context.
- IDictionary GetDefaultCorsHeaders(HttpContext httpContext);
- }
-}
diff --git a/MediaBrowser.Controller/Net/IWebSocketManager.cs b/MediaBrowser.Controller/Net/IWebSocketManager.cs
new file mode 100644
index 000000000..e9f00ae88
--- /dev/null
+++ b/MediaBrowser.Controller/Net/IWebSocketManager.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Jellyfin.Data.Events;
+using Microsoft.AspNetCore.Http;
+
+namespace MediaBrowser.Controller.Net
+{
+ ///
+ /// Interface IHttpServer.
+ ///
+ public interface IWebSocketManager
+ {
+ ///
+ /// Occurs when [web socket connected].
+ ///
+ event EventHandler> WebSocketConnected;
+
+ ///
+ /// Inits this instance.
+ ///
+ /// The websocket listeners.
+ void Init(IEnumerable listeners);
+
+ ///
+ /// The HTTP request handler.
+ ///
+ /// The current HTTP context.
+ /// The task.
+ Task WebSocketRequestHandler(HttpContext context);
+ }
+}
--
cgit v1.2.3
From 2f79c3095bb742136ff83141f42e344b33c3a45f Mon Sep 17 00:00:00 2001
From: Claus Vium
Date: Thu, 3 Sep 2020 11:54:38 +0200
Subject: Fix startup message
---
Emby.Server.Implementations/ApplicationHost.cs | 4 +++-
.../Middleware/ServerStartupMessageMiddleware.cs | 13 ++++++++++++-
Jellyfin.Server/Startup.cs | 3 +--
MediaBrowser.Controller/IServerApplicationHost.cs | 2 ++
4 files changed, 18 insertions(+), 4 deletions(-)
(limited to 'MediaBrowser.Controller')
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index c8af6b73a..8e9a581ea 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -132,6 +132,8 @@ namespace Emby.Server.Implementations
///
public bool CanSelfRestart => _startupOptions.RestartPath != null;
+ public bool CoreStartupHasCompleted { get; private set; }
+
public virtual bool CanLaunchWebBrowser
{
get
@@ -446,7 +448,7 @@ namespace Emby.Server.Implementations
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
Logger.LogInformation("Core startup complete");
-
+ CoreStartupHasCompleted = true;
stopWatch.Restart();
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
diff --git a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs b/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs
index 4f347d6d3..ea81c03a2 100644
--- a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs
+++ b/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs
@@ -1,5 +1,6 @@
using System.Net.Mime;
using System.Threading.Tasks;
+using MediaBrowser.Controller;
using MediaBrowser.Model.Globalization;
using Microsoft.AspNetCore.Http;
@@ -25,10 +26,20 @@ namespace Jellyfin.Server.Middleware
/// Executes the middleware action.
///
/// The current HTTP context.
+ /// The server application host.
/// The localization manager.
/// The async task.
- public async Task Invoke(HttpContext httpContext, ILocalizationManager localizationManager)
+ public async Task Invoke(
+ HttpContext httpContext,
+ IServerApplicationHost serverApplicationHost,
+ ILocalizationManager localizationManager)
{
+ if (serverApplicationHost.CoreStartupHasCompleted)
+ {
+ await _next(httpContext).ConfigureAwait(false);
+ return;
+ }
+
var message = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 80f679420..c197888da 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -123,6 +123,7 @@ namespace Jellyfin.Server
app.UseCorsOptionsResponse();
app.UseBaseUrlRedirection();
app.UseWebSocketHandler();
+ app.UseServerStartupMessage();
app.UseEndpoints(endpoints =>
{
@@ -133,8 +134,6 @@ namespace Jellyfin.Server
}
});
- app.UseServerStartupMessage();
-
// Add type descriptor for legacy datetime parsing.
TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter)));
}
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index d482c19d9..9f4c00e1c 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -20,6 +20,8 @@ namespace MediaBrowser.Controller
IServiceProvider ServiceProvider { get; }
+ bool CoreStartupHasCompleted { get; }
+
bool CanLaunchWebBrowser { get; }
///
--
cgit v1.2.3